2-25.【Concurrency】Concurrency 的任务取消是如何传播的?

4 阅读2分钟

1️⃣ 核心结论

在 Swift Concurrency 中,任务取消是协作式的,并通过父子任务树向下传播:父任务取消 → 所有直接和间接子任务收到取消信号 → 子任务在挂起点检查取消状态并自行终止。

一句话:取消是“向下传递的协作信号”,不是强制杀死线程。


2️⃣ 父子任务关系与结构化并发

  • Swift 使用 结构化并发(Structured Concurrency)管理任务生命周期

  • 父任务负责创建子任务:

    • async let 子任务
    • TaskGroup 内的任务
  • 父任务取消 → 子任务自动收到信号

Task {
    async let a = workA()
    async let b = workB()

    try await a + b
}.cancel() // 父任务取消
  • ab 都会被标记为取消
  • awaitTask.checkCancellation() 时响应

3️⃣ 协作式取消机制

  • Swift 取消是 协作式,不是强制杀死线程

  • 子任务必须显式检查:

    1. await 时自动检查
    2. Task.checkCancellation() 手动检查
Task {
    for i in 0..<1000 {
        try Task.checkCancellation()
        await doWork(i)
    }
}
  • 如果父任务取消 → Task.checkCancellation() 会抛 CancellationError
  • Task 可以安全终止,不会留下孤儿任务

4️⃣ TaskGroup 中的取消传播

await withThrowingTaskGroup(of: Int.self) { group in
    for i in 0..<5 {
        group.addTask { await work(i) }
    }
    // 取消父任务
}
  • 父任务取消 → 所有 group 内子任务标记取消
  • runtime 会将取消信号传递到每个子任务
  • 子任务在挂起点响应 → 终止或抛异常
  • group 在退出前会等待所有子任务完成或取消

5️⃣ DetachedTask 的区别

  • Task.detached 不属于父任务树
  • 不自动继承父任务取消
  • 取消必须手动控制:
let detached = Task.detached {
    for i in 0..<1000 {
        try Task.checkCancellation()
        await doWork(i)
    }
}

detached.cancel() // 需要显式取消

6️⃣ Runtime 的底层原理

  1. 父任务取消 → runtime 标记父任务状态为 canceled
  2. 遍历父任务的 子任务列表,逐一标记 isCancelled = true
  3. 子任务在下一个挂起点(awaitTask.checkCancellation())检查状态
  4. 子任务响应取消 → 抛出 CancellationError 或安全退出
  5. 父任务等待所有子任务完成或取消(join),保证结构化并发安全

7️⃣ 类比理解

  • 父任务 = “主管”
  • 子任务 = “员工”
  • 父任务取消 = “主管下达撤退信号”
  • 子任务挂起点 = “员工收到信号,可以安全停止工作”

8️⃣ 面试必背总结

  1. Swift 任务取消是 协作式,不是强制杀死线程
  2. 父子任务关系:父任务取消 → 子任务标记取消
  3. 挂起点检查:await 或 Task.checkCancellation() 响应
  4. TaskGroup / async let 都遵循取消传播
  5. DetachedTask 不继承父任务取消,需要手动控制