一句话先给结论
TaskGroup的设计目的,是在保持结构化并发的前提下,
安全地表达“一组并发执行、统一收敛结果的子任务”。
或者更狠一点的说法:
TaskGroup = “不会失控的 fork–join 并发”。
1️⃣ 为什么需要 TaskGroup?(设计动机)
先看一个“天真实现”的问题:
var results: [Int] = []
Task {
Task { results.append(await f1()) }
Task { results.append(await f2()) }
Task { results.append(await f3()) }
}
❌ 问题一堆:
results有数据竞争- 子任务生命周期不受控
- 取消无法统一传播
- 无法确定什么时候全部完成
TaskGroup 想解决的正是这些问题
let sum = await withTaskGroup(of: Int.self) { group in
group.addTask { await f1() }
group.addTask { await f2() }
group.addTask { await f3() }
var total = 0
for await value in group {
total += value
}
return total
}
2️⃣ TaskGroup 的核心设计目标
🎯 目标 1:结构化并发(Structured Concurrency)
-
所有子任务:
- 必须在 group 作用域内完成
- 不能逃逸
-
离开
withTaskGroup:- 要么所有完成
- 要么全部取消
👉 不会“偷偷活着的任务”
🎯 目标 2:安全地共享结果,而不是共享状态
TaskGroup 的哲学是:
并发任务之间不共享可变状态,只通过结果汇合
-
子任务:
- 不写共享变量
- 只
return值
-
父任务:
- 串行地消费结果
🎯 目标 3:明确的并发边界
-
并发只发生在:
group.addTask
-
收敛点是:
for await value in group
这让代码在语义上可推理。
3️⃣ TaskGroup 的工作模型(非常重要)
逻辑结构
Parent Task
└─ TaskGroup
├─ Child Task 1
├─ Child Task 2
└─ Child Task 3
特点:
-
子任务都是:
- 当前 task 的子 task
- 自动继承取消 / 优先级 / actor
生命周期规则(强约束)
| 行为 | 结果 |
|---|---|
| 父 task 被取消 | group 中所有子任务被取消 |
| 任一子任务抛错(throwing group) | 其余子任务全部取消 |
| 离开作用域 | 等待所有子任务完成 |
4️⃣ TaskGroup 如何保证并发安全?
这是你问题的核心 👇
✅ 1. 类型系统层面的隔离
withTaskGroup(of: Int.self) { group in
group.addTask {
// 只能返回 Int
}
}
- 子任务无法直接访问 group 的内部状态
- 只能通过 返回值通信
✅ 2. 串行消费结果
for await value in group {
// 这里是顺序执行的
}
- 即使子任务并发完成
- 结果的处理是单线程、顺序的
- 所以你可以安全地:
var sum = 0
sum += value
✅ 3. 作用域限制(无法逃逸)
你做不到:
var g: TaskGroup<Int>?
withTaskGroup(of: Int.self) { group in
g = group // ❌ 不允许
}
- group 不能被存储
- 不能被跨作用域使用
👉 这从根源上消灭了悬垂并发。
✅ 4. 自动取消传播
- 父取消 → 子取消
- 错误 → 全部取消
这避免了:
- 子任务写了一半共享状态
- 父任务已经不关心结果
✅ 5. 与 actor 隔离完美配合
actor Store {
func loadAll() async {
await withTaskGroup(of: Item.self) { group in
for id in ids {
group.addTask {
await fetchItem(id)
}
}
for await item in group {
self.items.append(item) // actor 内,安全
}
}
}
}
- 子任务不直接修改 actor 状态
- 所有 mutation 都在 actor 上顺序执行
5️⃣ TaskGroup vs async let(顺便对比)
| 特性 | TaskGroup | async let |
|---|---|---|
| 子任务数量 | 动态 | 静态 |
| 结果处理 | 流式 | 一次性 |
| 错误处理 | 可逐个 | 一起 |
| 适合场景 | 批量 / 不定量 | 少量固定 |
6️⃣ 常见误区(非常常见)
❌ 误区 1:TaskGroup 里共享变量是安全的
var sum = 0
withTaskGroup(of: Int.self) { group in
group.addTask { sum += 1 } // ❌ 数据竞争
}
子任务仍然是并发的。
❌ 误区 2:TaskGroup 会限制并发数量
不会。
如果你要限流,需要:
withTaskGroup { group in
// 自己控制 addTask 的节奏
}
或使用 semaphore / AsyncSemaphore。
7️⃣ 终极总结(可以直接背)
TaskGroup 的设计目的,是在结构化并发模型下,
安全地表达“多个并发子任务 + 统一收敛”的计算模式。
它通过作用域约束、结果传递而非状态共享、串行消费、自动取消传播,
从语言和类型系统层面避免了数据竞争和失控并发。