Swift Task 高级用法 全面深度讲解(Swift 6 最新版)

8 阅读12分钟

Task 是 Swift 结构化并发的核心执行单元,可彻底替代了 GCD 和 NSOperation。它不仅解决了回调地狱,更通过编译期安全、自动生命周期管理、内置取消机制、优先级继承等特性,从根本上解决了传统多线程的死锁、数据竞争、内存泄漏问题。

本文从底层原理 → 核心高级特性 → 实战场景 → 避坑指南逐层拆解,覆盖 Swift 6 最新特性,可生产环境落地。

一、Task 底层本质:不是 GCD 的封装

很多人误以为 Task 是 GCD 的语法糖,这是完全错误的。

1. 核心区别

特性Swift TaskGCD DispatchQueue
调度模型协作式线程池,由 Swift 运行时统一调度内核级线程池,由系统内核调度
线程映射多 Task 共享少量系统线程,无 1:1 对应关系一个 DispatchQueue 对应一个或多个系统线程
上下文切换用户态切换,开销极小内核态切换,开销大 10~100 倍
取消机制内置结构化取消,自动传播无原生取消,需手动实现
优先级自动优先级继承,解决优先级反转无优先级继承,易出现优先级反转
内存安全编译期 Sendable 检查,杜绝数据竞争运行时依赖手动加锁,易出错

2. Swift 运行时调度器原理

  • Swift 维护一个全局协作式线程池,线程数量等于 CPU 核心数(避免线程爆炸)
  • Task 是轻量级执行单元,内存开销仅几十字节
  • 当 Task 遇到 await 时,会让出线程给其他 Task 执行,不会阻塞线程
  • 线程永远不会空闲,最大化 CPU 利用率

二、核心高级特性 1:协作式取消机制(最强大也最容易用错)

1. 取消的本质:协作式,不是强制终止

let task = Task {
    while !Task.isCancelled { // 必须主动检查取消状态
        print("正在执行")
        try await Task.sleep(nanoseconds: 1_000_000_000)
    }
    print("任务被取消")
}

// 3秒后取消任务
try await Task.sleep(nanoseconds: 3_000_000_000)
task.cancel()

关键理解

  • task.cancel() 只是给任务打一个取消标记,不会强制终止任务
  • 任务必须主动检查取消状态并优雅退出
  • 所有系统异步 API(如 URLSession.data(from:))都已内置取消支持

2. 取消检查的三种方式

(1)Task.isCancelled:灵活控制

适合需要在取消时执行清理工作的场景:

func downloadFile(url: URL) async throws -> Data {
    var data = Data()
    for try await chunk in url.resourceBytes {
        if Task.isCancelled {
            // 取消时清理临时文件
            try FileManager.default.removeItem(at: tempFile)
            throw CancellationError()
        }
        data.append(chunk)
    }
    return data
}

(2)try Task.checkCancellation():快速抛出

适合不需要清理,直接终止的场景:

func processLargeData(_ data: Data) async throws {
    for i in 0..<1000 {
        try Task.checkCancellation() // 取消时直接抛出 CancellationError
        processChunk(data[i*1024..<(i+1)*1024])
    }
}

(3)withTaskCancellationHandler:取消时执行清理

当任务被取消时,自动执行指定的清理代码:

func startSocketConnection() async throws -> Data {
    let socket = Socket()
    return try await withTaskCancellationHandler {
        // 任务主体
        try await socket.connect()
        return try await socket.readData()
    } onCancel: {
        // 取消时自动关闭 socket
        socket.close()
    }
}

3. 取消的自动传播(结构化并发核心优势)

父任务取消 → 所有子任务自动取消,无需手动传递取消令牌:

let parentTask = Task {
    // 子任务1
    async let user = fetchUser()
    // 子任务2
    async let posts = fetchPosts()
    // 子任务3
    async let comments = fetchComments()
    
    return try await (user, posts, comments)
}

// 取消父任务 → 三个子任务全部自动取消
parentTask.cancel()

4. 常见取消坑点

忘记检查取消状态:任务会一直执行到结束,浪费资源❌ 在同步循环中不检查取消:无限循环会完全忽略取消❌ try? 吞掉 CancellationError:导致取消逻辑失效✅ 最佳实践:所有长时间运行的任务,每 100ms 至少检查一次取消状态

三、核心高级特性 2:优先级与优先级继承

1. Task 优先级与 GCD QoS 对应关系

TaskPriority对应 GCD QoS适用场景
.high.userInitiated用户点击触发的即时任务
.medium(默认).utility普通后台任务
.low.background非紧急后台任务
.userInteractive.userInteractiveUI 动画、实时交互

2. 优先级继承(解决优先级反转)

这是 Swift Task 比 GCD 最强大的特性之一:

当高优先级任务等待低优先级任务的结果时,低优先级任务会被临时提升到高优先级,避免高优先级任务被低优先级任务阻塞。

// 低优先级任务
let lowPriorityTask = Task(priority: .low) {
    print("低优先级任务开始")
    try await Task.sleep(nanoseconds: 2_000_000_000)
    print("低优先级任务结束")
    return "结果"
}

// 高优先级任务等待低优先级任务
Task(priority: .high) {
    print("高优先级任务开始等待")
    let result = await lowPriorityTask.value
    print("高优先级任务拿到结果:(result)")
}

执行结果:低优先级任务会被提升到 .high 优先级,立即执行,不会被其他低优先级任务抢占。

四、核心高级特性 3:结构化并发(async let + TaskGroup)

结构化并发的核心思想:任务的生命周期严格嵌套在父任务内,父任务结束前所有子任务必须完成,彻底杜绝野线程和资源泄漏。

1. async let:隐式任务组(固定数量并行)

适合已知数量的并行任务,语法最简洁:

func loadHomePage() async throws -> (User, [Post], [Comment]) {
    // 三个请求并行执行
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let comments = fetchComments()
    
    // 等待所有结果返回
    return try await (user, posts, comments)
}

特点

  • 自动创建子任务,自动等待完成
  • 任意一个子任务抛出错误 → 其他子任务自动取消 → 错误向上传播
  • 父任务取消 → 所有子任务自动取消

2. TaskGroup:动态任务组(动态数量并行)

适合循环生成的动态数量任务,如批量下载、批量请求:

func downloadImages(urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        // 动态添加任务
        for url in urls {
            group.addTask {
                try await downloadImage(url: url)
            }
        }
        
        // 收集结果(按完成顺序)
        var images: [UIImage] = []
        for try await image in group {
            images.append(image)
        }
        
        return images
    }
}

3. TaskGroup 高级用法

(1)限制最大并发数(避免 OOM)

一次性创建上千个 Task 会导致内存暴涨,必须限制并发数:

func downloadImagesThrottled(urls: [URL], maxConcurrent: Int = 4) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        var results: [UIImage] = []
        var index = 0
        
        // 先添加 maxConcurrent 个任务
        for _ in 0..<min(maxConcurrent, urls.count) {
            group.addTask { try await downloadImage(url: urls[index]) }
            index += 1
        }
        
        // 完成一个添加一个,保持最大并发数
        for try await result in group {
            results.append(result)
            if index < urls.count {
                group.addTask { try await downloadImage(url: urls[index]) }
                index += 1
            }
        }
        
        return results
    }
}

(2)部分任务失败不影响其他任务

使用 withTaskGroup(非 throwing),手动处理每个任务的错误:

func downloadImagesWithFallback(urls: [URL]) async -> [UIImage?] {
    await withTaskGroup(of: Result<UIImage, Error>.self) { group in
        for url in urls {
            group.addTask {
                Result { try await downloadImage(url: url) }
            }
        }
        
        var results: [UIImage?] = []
        for await result in group {
            switch result {
            case .success(let image): results.append(image)
            case .failure: results.append(nil)
            }
        }
        
        return results
    }
}

(3)提前取消所有任务

当获取到需要的结果后,立即取消剩余任务:

func findFirstAvailableServer(servers: [URL]) async throws -> URL {
    try await withThrowingTaskGroup(of: URL.self) { group in
        for server in servers {
            group.addTask {
                try await pingServer(server)
                return server
            }
        }
        
        // 拿到第一个成功的结果
        guard let first = try await group.next() else {
            throw NSError(domain: "NoServerAvailable", code: -1)
        }
        
        // 取消所有剩余任务
        group.cancelAll()
        return first
    }
}

五、核心高级特性 4:TaskLocal(任务本地存储)

TaskLocal 是任务树中的上下文传递机制,替代传统的 ThreadLocal,无需在每个函数参数中传递上下文数据。

1. 基本用法

// 1. 声明 TaskLocal(必须是静态属性,值必须 Sendable)
enum AppContext {
    @TaskLocal
    static var requestID: UUID?
    
    @TaskLocal
    static var userID: String?
}

// 2. 绑定值(仅在闭包范围内有效)
func handleRequest() async {
    let requestID = UUID()
    await AppContext.$requestID.withValue(requestID) {
        await AppContext.$userID.withValue("user123") {
            await processRequest()
        }
    }
}

// 3. 在任意子任务中访问
func processRequest() async {
    print("请求ID:(AppContext.requestID!)")
    print("用户ID:(AppContext.userID!)")
    
    // 子任务自动继承 TaskLocal 值
    async let subTask = subProcess()
    await subTask
}

2. 适用场景

  • 日志追踪:传递请求 ID、链路 ID
  • 用户上下文:传递当前登录用户信息
  • 环境配置:传递测试环境、生产环境标识
  • 语言 / 地区:传递当前本地化信息

3. 注意事项

  • TaskLocal 值自动继承给所有子任务
  • 绑定是词法作用域,仅在 withValue 闭包内有效
  • 嵌套绑定会覆盖外层值
  • 非结构化任务(Task.detached不会继承 TaskLocal 值

六、结构化 vs 非结构化任务

1. 结构化任务(Task { }

  • 继承父任务的优先级、Actor 上下文、TaskLocal、取消状态
  • 父任务取消 → 子任务自动取消
  • 编译器自动管理生命周期,无内存泄漏
  • 推荐优先使用

2. 非结构化任务(Task.detached { }

  • 不继承任何父任务上下文
  • 完全独立,生命周期手动管理
  • 父任务取消不影响非结构化任务
  • 仅适用于完全独立、不需要上下文的后台任务

对比表

特性Task { }(结构化)Task.detached { }(非结构化)
继承 Actor 上下文
继承优先级
继承 TaskLocal
取消自动传播
内存安全⚠️ 需手动管理
使用场景绝大多数场景完全独立的后台任务

七、与 GCD/Objective-C 互操作(老项目迁移必备)

1. 把基于回调的老 API 封装成 async/await

使用 withCheckedThrowingContinuation 桥接回调式 API:

// 老的回调式 API
func oldFetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
        } else if let data = data {
            completion(.success(data))
        }
    }.resume()
}

// 封装成 async/await
func newFetchData() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        oldFetchData { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

关键规则

  • continuation 必须且只能调用一次 resume
  • 多次调用会导致崩溃,不调用会导致内存泄漏
  • 支持取消:使用 withTaskCancellationHandler 在取消时调用 continuation.resume(throwing: CancellationError())

2. 在 GCD 中调用 async 函数

使用 Task { } 包裹:

DispatchQueue.global().async {
    // GCD 中调用 async 函数
    Task {
        let data = try await newFetchData()
        DispatchQueue.main.async {
            // 更新 UI
            self.label.text = String(data: data, encoding: .utf8)
        }
    }
}

3. 在 async 函数中调用 GCD 同步代码

使用 await 等待 GCD 任务完成:

func processDataInGCD() async -> Data {
    await withCheckedContinuation { continuation in
        DispatchQueue.global().async {
            let data = processData() // 同步耗时操作
            continuation.resume(returning: data)
        }
    }
}

八、Swift 6 Task 新特性

1. 严格并发检查(默认开启)

  • 所有跨任务传递的值必须符合 Sendable 协议
  • 编译期杜绝数据竞争
  • 非 Sendable 类型跨任务传递直接报错

2. Task.yield():让出执行权

主动让出当前线程给其他任务执行,提升并发性能:

func processLargeArray(_ array: [Int]) async {
    for i in 0..<array.count {
        processElement(array[i])
        
        // 每处理 100 个元素让出一次线程
        if i % 100 == 0 {
            await Task.yield()
        }
    }
}

3. 新的睡眠 API

// 旧 API(Swift 5.5+)
try await Task.sleep(nanoseconds: 1_000_000_000)

// 新 API(Swift 6+,更直观)
try await Task.sleep(for: .seconds(1))
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)

4. DiscardingTaskGroup

不需要收集结果的任务组,自动等待所有任务完成:

await withDiscardingTaskGroup { group in
    for url in urls {
        group.addTask {
            try await prefetchImage(url: url)
        }
    }
    // 自动等待所有预取任务完成
}

九、常见坑点与最佳实践

1. 绝对不要在 Task 中使用 GCD sync

// ❌ 会导致死锁
Task {
    DispatchQueue.main.sync {
        // 更新 UI
    }
}

// ✅ 正确:使用 @MainActor
Task { @MainActor in
    // 直接更新 UI
    self.label.text = "Hello"
}

2. 不要滥用 Task.detached

99% 的场景都应该使用结构化任务 Task { },只有当你明确需要一个完全独立的任务时才用 detached

3. 不要创建过多的小任务

每个 Task 都有少量内存开销,创建上千个小任务会导致内存暴涨。对于大量小任务,使用 TaskGroup 限制最大并发数。

4. 必须处理取消

所有长时间运行的任务都必须检查取消状态,尤其是循环和网络请求。

5. 不要在 @MainActor 中执行耗时操作

@MainActor 隔离的代码会在主线程执行,耗时操作会阻塞 UI。

6. 错误处理最佳实践

  • 不要用 try? 吞掉错误,尤其是 CancellationError
  • 使用 do/catch 明确处理不同类型的错误
  • 结构化任务中,错误会自动向上传播,在最上层统一处理

十、实战场景:可取消的批量图片下载

class ImageDownloader {
    private var downloadTask: Task<[UIImage?], Error>?
    
    func downloadImages(urls: [URL], maxConcurrent: Int = 4) async throws -> [UIImage?] {
        // 取消之前的下载任务
        downloadTask?.cancel()
        
        let task = Task {
            try await withThrowingTaskGroup(of: (Int, UIImage?).self) { group in
                var results = [UIImage?](repeating: nil, count: urls.count)
                var index = 0
                
                // 限制最大并发数
                for _ in 0..<min(maxConcurrent, urls.count) {
                    let currentIndex = index
                    group.addTask {
                        let image = try? await self.downloadImage(url: urls[currentIndex])
                        return (currentIndex, image)
                    }
                    index += 1
                }
                
                // 完成一个添加一个
                for try await (resultIndex, image) in group {
                    results[resultIndex] = image
                    
                    if index < urls.count && !Task.isCancelled {
                        let currentIndex = index
                        group.addTask {
                            let image = try? await self.downloadImage(url: urls[currentIndex])
                            return (currentIndex, image)
                        }
                        index += 1
                    }
                }
                
                return results
            }
        }
        
        downloadTask = task
        return try await task.value
    }
    
    func cancel() {
        downloadTask?.cancel()
    }
    
    private func downloadImage(url: URL) async throws -> UIImage {
        let (data, _) = try await URLSession.shared.data(from: url)
        guard let image = UIImage(data: data) else {
            throw NSError(domain: "InvalidImage", code: -1)
        }
        return image
    }
}

十一、常见问题

1. Swift Task 和 GCD 有什么区别?

Task 是 Swift 运行时的协作式线程池,GCD 是内核级线程池。Task 支持结构化取消、优先级继承、编译期数据竞争检查,比 GCD 更安全、更高效。

2. 什么是协作式取消?

取消只是给任务打一个标记,任务必须主动检查取消状态并优雅退出,系统不会强制终止任务。

3. 结构化并发的优势是什么?

任务生命周期严格嵌套,父任务结束前所有子任务必须完成;取消自动传播;编译器自动管理内存,杜绝野线程和资源泄漏。

4. TaskLocal 和 ThreadLocal 有什么区别?

TaskLocal 是任务级别的上下文传递,自动继承给所有子任务;ThreadLocal 是线程级别的,线程切换后会丢失。

5. 什么时候用 async let,什么时候用 TaskGroup?

固定数量的并行任务用 async let;动态数量的并行任务用 TaskGroup。

十二、总结

  1. Task 本质:Swift 运行时的协作式轻量级执行单元,不是 GCD 的封装
  2. 核心特性:结构化取消、优先级继承、自动生命周期管理、编译期安全
  3. 结构化并发:async let(固定数量)+ TaskGroup(动态数量),优先使用
  4. 上下文传递:TaskLocal 替代 ThreadLocal,无需参数透传
  5. 互操作:用 withCheckedContinuation 桥接老的回调式 API
  6. 避坑:不要用 GCD sync、不要滥用 detached、必须处理取消