Task 是 Swift 结构化并发的核心执行单元,可彻底替代了 GCD 和 NSOperation。它不仅解决了回调地狱,更通过编译期安全、自动生命周期管理、内置取消机制、优先级继承等特性,从根本上解决了传统多线程的死锁、数据竞争、内存泄漏问题。
本文从底层原理 → 核心高级特性 → 实战场景 → 避坑指南逐层拆解,覆盖 Swift 6 最新特性,可生产环境落地。
一、Task 底层本质:不是 GCD 的封装
很多人误以为 Task 是 GCD 的语法糖,这是完全错误的。
1. 核心区别
| 特性 | Swift Task | GCD 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 | .userInteractive | UI 动画、实时交互 |
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。
十二、总结
- Task 本质:Swift 运行时的协作式轻量级执行单元,不是 GCD 的封装
- 核心特性:结构化取消、优先级继承、自动生命周期管理、编译期安全
- 结构化并发:async let(固定数量)+ TaskGroup(动态数量),优先使用
- 上下文传递:TaskLocal 替代 ThreadLocal,无需参数透传
- 互操作:用 withCheckedContinuation 桥接老的回调式 API
- 避坑:不要用 GCD sync、不要滥用 detached、必须处理取消