深入结构化并发:取消、优先级与任务组
Beyond the basics of structured concurrency
2023年6月5日
一句话判断
如果你已经用了 async/await 但对任务取消、优先级传播和任务组资源管理还有疑问,这场 Session 用汤厨的比喻把这些概念讲清楚了——特别是 withTaskCancellationHandler 在 AsyncSequence 中的用法值得反复看。
这场 Session 讲了什么
Session 以”厨房做汤”为贯穿全场的例子,系统讲解结构化并发的三个核心机制:任务取消(Task Cancellation)、任务层级(Task Hierarchy)和任务组(Task Groups)的资源管理。
结构化任务通过 async let 和 Task Group 创建,它们与父任务形成树状层级关系。非结构化任务(Task.init 和 Task.detached)不参与这个层级。结构化任务的生命周期绑定到声明它们的作用域,离开作用域时自动取消。
任务取消是协作式的——取消只设置标志位,不会强制停止。你的代码需要主动检查 Task.isCancelled 或调用 Task.checkCancellation()(后者在已取消时直接抛出错误)。关键点在于:取消父任务会递归取消所有子任务。
withTaskCancellationHandler 处理了一个微妙场景:当任务挂起等待 AsyncSequence 的下一个元素时,无法通过轮询检查取消状态。取消处理器让你在取消发生时立即执行清理逻辑,比如停止驱动 AsyncSequence 的状态机。但要注意,取消处理器与主逻辑共享状态,需要用原子操作(Swift Atomics)、锁或 Dispatch Queue 来保护。
Session 还讨论了任务组中的资源管理——如何限制并发任务数量、如何在任务组中使用 task-local values 进行追踪和性能分析。
值得深挖的点
取消处理器中的共享状态问题:这是结构化并发中最容易出 bug 的地方。取消处理器与主逻辑并发运行,如果状态机没有正确同步,可能导致竞态条件。Session 明确推荐使用 Swift Atomics 包而非 Actor,因为 Actor 不保证操作顺序,无法确保取消处理先于主逻辑执行。
结构化 vs 非结构化任务的选择:结构化任务能自动享受取消传播、优先级继承和 task-local value 传递。非结构化任务(Task.init)虽然灵活,但需要手动管理这些特性。尽可能优先使用结构化任务。
checkCancellation vs isCancelled:前者抛出错误,适合你打算中止任务时使用;后者返回布尔值,适合你想返回部分结果而非抛出错误的场景。
代码片段
在 AsyncSequence 中处理任务取消:
// 使用取消处理器处理挂起时的取消
func processOrders() async throws {
let orderStream = OrderStream()
try await withTaskCancellationHandler {
// 主逻辑:循环处理订单
for try await order in orderStream {
try makeSoup(from: order) // makeSoup 内部会检查取消状态
}
} onCancel: {
// 取消时停止状态机(注意:这里是并发执行的!)
// 需要使用原子操作或锁来保护共享状态
orderStream.cancel()
}
}
在任务组中检查取消状态:
func chopIngredients(_ ingredients: [Ingredient]) async throws -> [ChoppedIngredient] {
try await withThrowingTaskGroup(of: ChoppedIngredient.self) { group in
for ingredient in ingredients {
group.addTask {
// 在昂贵操作前检查取消状态
try Task.checkCancellation()
return try await chop(ingredient)
}
}
var result: [ChoppedIngredient] = []
for try await chopped in group {
result.append(chopped)
}
return result
}
}
最佳实践
- 在每个耗时操作前检查取消状态,避免做无用功
- 使用
withTaskCancellationHandler处理 AsyncSequence 等挂起场景下的取消 - 取消处理器中的共享状态必须用原子操作或锁保护,不能用 Actor
- 优先使用结构化任务(
async let和 Task Group),享受自动取消传播 - 在服务端环境中,利用 task-local values 实现请求级别的追踪和日志关联
还有什么值得关注
- 任务优先级如何在任务层级中传播
- Task Group 中限制并发任务数量的模式
Task.detached在什么场景下是唯一正确的选择- Swift Atomics 包在实际项目中的使用体验