一句话概括
Swift Concurrency 强调“结构化”,就是让所有并发任务有明确的父子关系和生命周期,避免任务“漂浮”或泄露资源,从而保证并发可推理、安全、可取消。
1️⃣ 问题背景:非结构化并发的痛点
传统线程 / 线程池模型:
executor.submit {
doWork()
}
问题:
-
任务孤儿化
- 父函数返回后,任务可能还在跑
- 没人管理它的生命周期
-
资源泄露
- 内存 / 文件句柄 / actor 状态可能没被释放
- 如果任务一直挂起,资源永远占用
-
取消困难
- 父任务取消无法自动影响子任务
- 必须手动 propagate
-
错误传播混乱
- 子任务失败可能被忽略
2️⃣ “结构化”的核心思想
Structured Concurrency 的原则:
-
父子任务树
- 每个任务都有父任务
- 父任务控制子任务的生命周期
-
作用域边界约束
- 子任务必须在创建它的作用域内完成
- 作用域结束 = 等待所有子任务完成
-
自动取消传播
- 父取消 → 所有子任务自动收到取消信号
-
错误结构化传播
- Throwing TaskGroup 中任何子任务抛错
- 其余子任务自动取消
- 父任务立即知道
3️⃣ Swift 是怎么做的
① Task 树(父子关系)
Task {
Task {
await work1()
}
Task {
await work2()
}
}
- 父 task 等待子 task 完成
- 生命周期绑定 → 不会出现“幽灵任务”
② TaskGroup 的作用域控制
await withTaskGroup(of: Int.self) { group in
group.addTask { await fetchA() }
group.addTask { await fetchB() }
for await value in group { ... } // 处理结果
} // group 作用域结束,所有任务结束
- 离开作用域 = 自动 join + 清理
- 结果收集安全,避免共享可变状态
③ 自动取消
let parent = Task {
try await withThrowingTaskGroup(of: Int.self) { group in
group.addTask { try await work1() }
group.addTask { try await work2() }
...
}
}
parent.cancel()
- 父任务取消 → 所有子任务被标记取消
- 子任务在下一次
await检查取消时退出 - 不会悬挂,资源释放
④ actor / executor 与隔离安全
- 子任务继承父 executor(比如 MainActor)
- 离开作用域时,actor 上的资源依然受控制
- 避免 detached task 导致 actor 状态被“野指针”访问
4️⃣ 为什么结构化减少资源泄露
-
生命周期清晰
- 父作用域结束前必须完成所有子任务
- 自动释放 async context、堆内存、局部变量
-
取消协作机制
- 不再需要手动 cancel,每个任务都响应取消
-
结果收敛
- 不共享可变状态 → 不会有 partially written 内存
-
作用域结束 = join + cleanup
- 就像 RAII(资源获取即初始化)在同步代码中一样
5️⃣ 为什么结构化避免任务孤儿化
-
DetachedTask:可能活得比父任务久 → 孤儿
-
Structured Task / TaskGroup:
- 父任务作用域结束 → 子任务必须完成
- 生命周期绑定 → 任务不会漂浮
6️⃣ 形象比喻
-
非结构化并发 = “放风筝”
- 放出去就不管它,风吹哪去不确定
-
结构化并发 = “树枝上结果子”
- 子任务挂在父任务树上,父任务收集、管理、清理
7️⃣ 面试必背总结(浓缩版)
Swift Concurrency 强调结构化,是为了确保每个并发任务有明确父任务和作用域,生命周期受控、错误和取消可传播,从而避免资源泄露和任务孤儿化,使并发代码安全、可推理、可管理。