11-16.【错误处理】在 Swift Concurrency 中,async throws 如何和 Task/TaskGroup 的错误传播结合?

2 阅读3分钟

在 Swift Concurrency 中,async throwsTaskTaskGroup 的结合不仅仅是简单的错误传递,它涉及到一套**协作式(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)中,错误的传播路径是非常明确的:

  1. 向上冒泡:子任务的错误会自动传播给父任务。
  2. 清理机制:在错误离开当前作用域之前,Swift 保证所有的子任务都会被等待(Awaited)或取消。
  3. 结果转换:在底层,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. 总结:设计模型对比

特性同步 throwsasync throws (TaskGroup)
传播范围当前调用栈整个任务树(父子任务)
副作用仅中断当前函数触发兄弟任务的取消
错误载体寄存器 (x21)任务上下文 (Task Context / Result)
开发者责任处理错误处理错误 + 检查取消状态

底层深度提示:在 Swift 运行时中,当 async throws 函数抛出错误时,它会执行一个名为 swift_errorRetain 的操作,确保错误对象在跨线程恢复(Resume)过程中依然有效。