在 Swift Concurrency 中,async throws 与 Task 或 TaskGroup 的结合不仅仅是简单的错误传递,它涉及到一套**协作式(Cooperative)**的错误传播和任务生命周期管理机制。
理解这一点的关键在于:错误(Error)不仅是逻辑的分支,它还是任务取消(Cancellation)的触发器。
1. TaskGroup 的错误传播:一荣俱荣,一损俱损
ThrowingTaskGroup 是处理并发错误的核心。它的行为逻辑是:任何一个子任务的失败都会导致整个组进入“取消”状态。
- 短路效应(Short-circuiting) :当你在
TaskGroup中使用try await group.next()时,如果某个子任务抛出了错误,该错误会立即从withThrowingTaskGroup块中冒泡出来。 - 协作取消:一旦某个任务报错,Group 会自动向所有其他尚未完成的子任务发送“取消信号”。
- 注意点:取消信号只是一个标志位。如果你的子任务里没有检查
Task.isCancelled,它们会继续消耗资源运行,直到完成。
Swift
try await withThrowingTaskGroup(of: Data.self) { group in
group.addTask { try await fetch(id: 1) }
group.addTask { try await fetch(id: 2) }
// 如果 fetch(1) 报错,这里会立即抛出错误
// 并且 fetch(2) 会被标记为已取消 (Cancelled)
while let data = try await group.next() {
process(data)
}
}
2. 结构化并发中的错误“冒泡”路径
在结构化并发(Structured Concurrency)中,错误的传播路径是非常明确的:
- 向上冒泡:子任务的错误会自动传播给父任务。
- 清理机制:在错误离开当前作用域之前,Swift 保证所有的子任务都会被等待(Awaited)或取消。
- 结果转换:在底层,
async throws的错误被存储在Task的内部存储区中。当你调用try await task.value时,运行时会检查该存储区,如果有错误则重新抛出(Rethrow)。
3. 非结构化 Task 的错误陷阱
对于 Task { ... } 或 Task.detached { ... } 这种非结构化任务,错误传播的行为会有所不同:
- 隐式捕获:错误会被静默捕获在
Task实例内部。 - 风险:如果你创建了一个
Task但不持有它的引用,也不await它的结果,那么发生的任何throws都会被忽略,这在调试时非常致命。 - 防御式做法:始终处理
Task的返回值,或者在Task内部使用do-catch记录日志。
Swift
let myTask = Task {
try await performCriticalOp()
}
// 必须通过这种方式感知错误
let result = await myTask.result // 返回 Result<Success, Error>
4. 性能与防御式建议
A. 优先使用 withThrowingTaskGroup 而非多个独立 Task
因为 TaskGroup 提供了自动的取消传播。如果其中一个请求因为网络超时报错,你肯定不希望其他 10 个请求继续无意义地浪费电量和带宽。
B. 利用 CheckedContinuation 桥接旧代码时要小心
如果你在 async throws 中使用 withCheckedThrowingContinuation:
- 防御原则:必须且只能调用一次
resume(无论是throwing还是returning)。 - 如果忘记调用
resume,任务会永久挂起;如果调用多次,程序会崩溃。这是async throws体系中最危险的边界。
C. 在失败路径中利用 Task.checkCancellation()
由于错误传播会导致其他任务取消,你的业务代码应当对“取消”有感知。
Swift
group.addTask {
let parts = try await downloadParts()
// 在进入耗时的 CPU 密集操作前检查,避免无效计算
try Task.checkCancellation()
return combine(parts)
}
5. 总结:设计模型对比
| 特性 | 同步 throws | async throws (TaskGroup) |
|---|---|---|
| 传播范围 | 当前调用栈 | 整个任务树(父子任务) |
| 副作用 | 仅中断当前函数 | 触发兄弟任务的取消 |
| 错误载体 | 寄存器 (x21) | 任务上下文 (Task Context / Result) |
| 开发者责任 | 处理错误 | 处理错误 + 检查取消状态 |
底层深度提示:在 Swift 运行时中,当 async throws 函数抛出错误时,它会执行一个名为 swift_errorRetain 的操作,确保错误对象在跨线程恢复(Resume)过程中依然有效。