2-26.【Concurrency】在 TaskGroup 中,如何正确响应取消信号?

1 阅读2分钟

1️⃣ 核心原则

TaskGroup 中的子任务会自动继承父任务的取消信号,但子任务必须在挂起点(awaitTask.checkCancellation())显式响应取消,才能安全终止并释放资源。

一句话:父任务取消 → 子任务标记为 cancelled → 子任务在挂起点响应 → TaskGroup 安全退出


2️⃣ 基本示例

try await withThrowingTaskGroup(of: Int.self) { group in
    for i in 0..<5 {
        group.addTask {
            for j in 0..<1000 {
                try Task.checkCancellation()   // 响应取消
                await doWork(i, j)
            }
            return i
        }
    }

    var results: [Int] = []
    for try await value in group {
        results.append(value)
    }
}
  • 父任务或外层 Task 调用 .cancel() → 所有子任务 isCancelled = true
  • 子任务在 Task.checkCancellation()await 时抛出 CancellationError → 安全退出
  • TaskGroup 会等待所有子任务完成或被取消后才返回

3️⃣ 关键注意点

① 必须在挂起点检查取消

  • 挂起点包括:

    • await 调用
    • Task.sleep
    • Task.checkCancellation()
  • 没有检查的长循环或同步操作不会自动取消,可能阻塞 TaskGroup 退出

group.addTask {
    for i in 0..<1_000_000 {
        try Task.checkCancellation()  // 必须
        doHeavyWork(i)
    }
}

② 父任务取消 → TaskGroup 自动 propagate

let parentTask = Task {
    try await withThrowingTaskGroup(of: Int.self) { group in
        group.addTask { await work1() }
        group.addTask { await work2() }
        return try await group.reduce(0, +)
    }
}

parentTask.cancel() // 所有 group 子任务收到取消信号
  • 子任务在下一个挂起点响应取消
  • 父任务等待所有子任务完成 → TaskGroup 安全退出
  • 避免资源泄漏和孤儿任务

③ 使用 withTaskCancellationHandler 做清理(可选)

group.addTask {
    try await withTaskCancellationHandler {
        print("Clean up resources on cancel")
    } operation: {
        for i in 0..<1000 {
            try Task.checkCancellation()
            await doWork(i)
        }
    }
}
  • 保证取消时释放资源
  • TaskGroup 退出前会执行清理操作

④ 不要在子任务中做 CPU 密集型同步阻塞

  • 同步长计算不会自动响应取消
  • 可使用 Task.detached 或定期调用 Task.checkCancellation()

4️⃣ 总结策略

  1. 在 TaskGroup 内部添加子任务时,确保子任务在循环或异步操作中定期检查取消
  2. 使用 await 调用或 Task.checkCancellation() 响应取消
  3. 父任务取消 → 子任务自动标记 cancelled → TaskGroup 等待所有子任务完成
  4. 必要时使用 withTaskCancellationHandler 做资源清理

5️⃣ 面试必背总结

在 TaskGroup 中,子任务会继承父任务的取消状态,但必须在挂起点显式检查取消(awaitTask.checkCancellation())。父任务取消会 propagate 到所有子任务,TaskGroup 会等待它们完成或取消,从而保证资源安全和结构化并发正确性。