为什么需要 TaskGroup?
在 Swift 并发里,当你想:
- 同时发起 N 个异步任务(如批量下载图片)
- 等全部完成再汇总结果
- 支持中途取消、错误传播
手动 Task { }
会显得碎片化且难以管理。
withTaskGroup
/ withThrowingTaskGroup
提供官方"任务组"抽象:
- 统一并发上限(线程池复用)
- 自动生命周期管理,无内存泄漏
- 支持结构化取消与错误聚合
Swift 6.1 的新语法亮点
版本 | 声明方式 | 是否必填子任务返回类型 |
---|---|---|
≤ Swift 6.0 | withTaskGroup(of: Int.self) |
✅ 必须显式 of: Type.self |
Swift 6.1 | withTaskGroup { group in ... } |
❌ 编译器自动推断 |
→ 去掉 of:
后,代码更短、重构更安心;复杂元组/struct 类型不再写两遍。
最小可运行示例:类型推断实战
- 简单数据并行
swift
func fetchNumbers() async -> [Int] {
await withTaskGroup { group in // ← 不再需要 of: Int.self
for i in 1...3 {
group.addTask { // 返回 Int
return i * 2
}
}
var results = [Int]()
for await value in group {
results.append(value)
}
return results.sorted()
}
}
// 使用
Task {
let nums = await fetchNumbers()
print(nums) // [2, 4, 6]
}
- 复杂类型同样受益
swift
struct ImageInfo { let url: URL, size: Int, checksum: UInt32 }
func batchDownload(_ urls: [URL]) async -> [ImageInfo] {
do {
let infos = try await withThrowingTaskGroup { group in // 自动推断 ImageInfo
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
return ImageInfo(
url: url,
size: data.count,
checksum: 2
)
}
}
var infos = [ImageInfo]()
for try await info in group {
infos.append(info)
}
return infos
}
return infos
} catch {
print(error.localizedDescription)
}
return []
}
→ 若返回类型日后增加字段,无需改动 withTaskGroup
声明。
ThrowingTaskGroup:允许子任务抛出错误
- 基础示例(Swift 6.1 同样省
of:
)
swift
func fetchUserInfo(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup { group in // 推断 String & User
for id in ids {
group.addTask {
return try await API.user(id: id) // 可能抛出
}
}
var users = [User]()
for try await user in group { // 注意 try
users.append(user)
}
return users
}
}
- 错误传播规则
- 任一子任务
throw
→ 整个组立即throw
(默认) - 想收集所有成功 + 单独处理失败 → 用
group.addTaskUnlessCancelled
+do catch
内部吞错
swift
for try await user in group {
// 这里 throw 会**中断**后续任务
}
→ 需要"全部完成再统一处理"请参考 group.next()
手动迭代。
与 Swift 6 并发隔离的兼容
TaskGroup 的 addTask
继承调用者的隔离域:
swift
@MainActor
class VM {
func loadImages() async {
await withTaskGroup { group in // 主线程创建
for url in urls {
group.addTask { // 子任务**脱离**主线程
let data = try await URLSession.shared.data(from: url).0
return UIImage(data: data)!
}
}
// 汇总时可回到主线程
for await img in group {
self.images.append(img)
}
}
}
}
→ 网络下载在全局执行器,汇总后 images
回主线程,安全。
六、常见坑 & 最佳实践
陷阱 | 说明 | 修复 |
---|---|---|
忘记 for await 导致永远挂起 |
组内任务完成但结果未被消费 | 务必用 for await 或 while let value = await group.next() |
捕获 self 造成循环引用 |
addTask { self.view.reload() } |
使用 [weak self] 或先把数据拉出来 |
想"无论成败都收集" | 默认 for try await 一遇 throw 就中断 |
内部 do catch 手动 resume 成功/失败数组 |
需要限制并发数量 | withTaskGroup 会全开 |
使用 AsyncSequence + AsyncChannel 或信号量 |
一句话总结
"批量并发用 Group,Swift 6.1 起别再写 of:
。"
记住口诀:"普通组用 withTaskGroup
,会抛错就 withThrowingTaskGroup
;结果靠 for await
扫,错误用 try
收。"
去掉类型标注后,代码更短、重构更安心;让编译器多干活,你少写两行!