Beyond the basics of structured concurrency
System & Services 进阶 20m

深入结构化并发:取消、优先级与任务组

Beyond the basics of structured concurrency

2023年6月5日

在 Apple 官方观看视频

一句话判断

如果你已经用了 async/await 但对任务取消、优先级传播和任务组资源管理还有疑问,这场 Session 用汤厨的比喻把这些概念讲清楚了——特别是 withTaskCancellationHandler 在 AsyncSequence 中的用法值得反复看。

这场 Session 讲了什么

Session 以”厨房做汤”为贯穿全场的例子,系统讲解结构化并发的三个核心机制:任务取消(Task Cancellation)、任务层级(Task Hierarchy)和任务组(Task Groups)的资源管理。

结构化任务通过 async let 和 Task Group 创建,它们与父任务形成树状层级关系。非结构化任务(Task.initTask.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 包在实际项目中的使用体验
WWDC 2023