在 Swift Concurrency(async/await)模型中,async throws 的组合并不是简单的“异步 + 错误处理”的叠加,而是在控制流、性能以及任务生命周期上引入了新的规则。
以下是设计和使用 async throws 时的核心注意事项:
1. 错误传播与任务取消 (Cancellation)
在异步函数中,错误抛出通常与任务取消交织在一起。
- 隐式检查:当一个
async throws函数内部抛出错误时,它会自动中止当前任务。但更重要的是,如果外部取消了任务,许多系统 API(如Task.checkCancellation())会主动抛出CancellationError。 - 注意事项:在编写长耗时的异步循环时,你应该防御性地检查取消状态。如果任务已取消,抛出错误是终止执行的最佳方式。
Swift
func processLargeData() async throws {
for item in data {
// 防御式编程:在继续之前检查任务是否还合法
try Task.checkCancellation()
try await process(item)
}
}
2. 堆栈回溯与“挂起点”
与同步 throws 不同,async throws 的执行可能会跨越多个线程。
- 挂起点(Suspension Points) :每一个
try await都是一个潜在的挂起点。 - 性能影响:在同步代码中,
throws只是简单的寄存器跳转。而在异步代码中,如果await的函数抛出错误,运行时需要恢复之前保存的异步上下文(Continuation) ,并将错误注入该上下文。这比同步抛出稍微昂贵一些,因为它涉及状态机的切换。
3. 避免“双重失败”陷阱
在处理异步任务组(Task Groups)时,错误处理变得复杂:
- 首位获胜原则:在一个
ThrowingTaskGroup中,如果其中一个子任务抛出错误,整个 Group 会被标记为失败。 - 清理责任:即使一个子任务抛出错误,其他已经在运行的子任务不会立即停止,除非你在代码中显式检查取消。
Swift
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await task1() }
group.addTask { try await task2() }
// 如果 task1 崩了,task2 依然在后台跑,除非你调用了 Task.checkCancellation()
try await group.waitForAll()
}
4. 结果包装:Task 与 Result 的转换
当你从一个非异步环境启动一个异步任务时,错误处理的边界会发生变化:
- Task 句柄:
let task = Task { try await fetchData() }。此时,错误被封装在task.result中(类型为Result<Success, Error>)。 - 注意事项:如果你忘记在读取
task.value时使用try,或者没有处理task.result,错误就会被静默忽略。这是异步防御式编程中的常见漏洞。
5. 类型化抛出 (Typed Throws) 的新优势
在 Swift 6.0+ 中,async throws(MyError) 对异步函数尤为重要:
- 泛型约束:在处理复杂的异步流(AsyncSequence)时,明确的错误类型可以让编译器验证你的
catch块是否覆盖了所有可能的异步异常。 - 性能优化:减少了异步上下文恢复时对
any Error进行动态拆箱的开销。
总结:async throws 最佳实践清单
- 始终检查取消:在
async throws循环内部调用try Task.checkCancellation()。 - 优雅降级:在
await之后,考虑是否可以用try?或??快速提供默认值,以避免不必要的错误冒泡导致整个任务链崩溃。 - 明确清理:利用
defer块确保即使异步函数抛出错误,资源(如数据库连接、文件句柄)也能被正确关闭。 - 优先使用 async/await 而非 Result 闭包:它消除了回调地狱中的“忘记调用 completion”导致的错误丢失风险。