首页 / Swift 编程语言教程 / Task 与任务取消

Swift 编程语言教程

Task 与任务取消

本教程共 93 篇 · 第 82 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

SwiftSwift 编程语言教程并发Task任务取消checkCancellationisCancelled非结构化并发

本节目标:理解 Task 作为并发基本单位的层级关系,掌握创建非结构化任务,以及用协作式取消让长任务能被用户中途停下。

前面用 async let 和任务组时,任务的创建和销毁被语法自动管好了。但有时候你需要更主动地掌控一个任务——比如从同步代码里启动并发、拿到句柄后随时取消它。这一章专门讲 Task 本身,以及它的取消机制。

1-1 Task 是并发的基本单位

Swift 里所有异步代码都跑在某个 Task 里。Task 是”一份可以异步执行的工作”。一个 Task 同一时刻只做一件事,但你创建多个 Task,Swift 就能把它们调度到不同核心上同时跑。

Task 之间有层级。一个任务组里的每个子任务,都有一个共同的父任务;每个任务又可以有自己的子任务。这种父子树形结构就是”结构化并发”的基础——父等子、父子同取消,都靠这层关系。

async letwithTaskGroup 隐式创建的是子任务,被父任务牢牢管着。如果你想更自由地管理,比如脱离当前结构单独开一个任务,就要用到下面两种。

1-2 非结构化任务:Task 与 Task.detached

从同步代码启动异步工作,最常见的是 Task {} 初始化器。它创建的是一个非结构化任务(没有父任务),但默认继承当前任务的 actor 隔离、优先级和任务本地状态:

let handle = Task {
    let result = await fetchAndSave()
    return result
}
let value = await handle.value   // 等待结果

Task {} 返回的是一个句柄,你可以 await handle.value 等它出结果,也可以 handle.cancel() 取消它。

如果你想开一个更”独立”的任务,用 Task.detached {}。它继承当前 actor 隔离、优先级和任务本地状态,从一个干净的状态开始。绝大多数业务场景用 Task {} 就够了,需要彻底脱离上下文时才用 detached

Note

非结构化任务给了你完全的自由,代价是正确性是你的责任。没有父任务替你等、替你取消,忘记处理就可能留下”孤儿任务”。所以能用结构化写法(async let/任务组)就尽量用结构化。

1-3 取消是协作式的,不是强杀

Swift 的取消是协作式(cooperative)的:调用 cancel() 并不会像”杀进程”那样立刻终止任务。它只是给任务打个”请停止”的标记,任务自己要在合适的点检查这个标记,然后主动停下来。

这就像你跟同事说”这活不做了”,同事得自己听到、自己收手,而不是你直接把他的电脑拔了。为什么要这么设计?因为突然中断可能让数据处于半改状态,留下隐患。协作式取消让任务有机会干净地收尾。

1-4 用 checkCancellation 抛错式取消

任务体里可以调用 Task.checkCancellation()。如果被取消了,它直接抛出 CancellationError,任务随之停下。这对”抛错型”任务最省事:

func slowWork() async throws {
    for i in 1...5 {
        try await Task.sleep(nanoseconds: 300_000_000)
        try Task.checkCancellation()   // 取消就在这里抛错
        print("完成第 \(i) 步")
    }
}

一旦外部调用 task.cancel(),下一次循环跑到 try Task.checkCancellation() 就会抛出,函数整体退出,没做的步骤自然停了。抛错的好处是简单、易懂,错误能自动向上传播。

1-5 用 isCancelled 做清理式取消

如果你在停之前还想干点收尾(关连接、删临时文件),就用 Task.isCancelled 属性读取标记,自己做判断:

func downloadWithCleanup() async {
    guard !Task.isCancelled else { return }
    let connection = openConnection()
    defer { connection.close() }   // 无论如何都关闭连接

    while let chunk = await connection.nextChunk() {
        if Task.isCancelled {
            print("被取消,丢弃已下载部分")
            return
        }
        save(chunk)
    }
}

这里每次循环都检查 isCancelled,取消了就打印提示并 returndefer 保证连接被关掉。比抛错灵活,适合需要清理的场合。

Tip

Task.checkCancellation() 适合”取消就直接失败”的简单任务;Task.isCancelled 适合”取消前要做点清理”的任务。二选一,看你要不要优雅收尾。

1-6 取消沿任务树传播

取消最省心的一点是自动向下传播。父任务被取消,它的所有子任务(无论是 async let 还是任务组里的)也一并被取消。你不用手动一个个去取消。

let parent = Task {
    async let a = longTaskA()
    async let b = longTaskB()
    _ = await [a, b]
}
parent.cancel()   // a 和 b 也会收到取消信号

反过来不成立:取消某个子任务不影响父任务和其他兄弟。这让”取消一个整体操作”变得很自然——取消最外层任务,内部全部跟着停。

1-7 取消处理器:即时响应通知

如果任务内部是阻塞式等待,想在被取消那一刻立刻收到通知做点事,可以用 withTaskCancellationHandler

let task = Task {
    await withTaskCancellationHandler {
        await doSomeWork()
    } onCancel: {
        print("收到取消通知!")
    }
}
// 稍后
task.cancel()   // 打印:收到取消通知!

onCancel 闭包在取消发生时被调用。但要注意:任务本身仍是协作式的,它要么跑完、要么自己检查取消后停下。onCancel 只是个通知钩子,闭包里不要和任务共享可变状态,否则会引入新的竞争。

Warning

别以为 cancel() 一调用任务就立刻没了。如果任务体里从没检查 isCancelled、也从没写 try Task.checkCancellation(),它根本不知道自己被取消了,会一路跑完。所以写长任务时,务必在循环或适当节点埋入取消检查。

1-8 任务优先级与值传递

任务还有优先级(priority)概念,告诉调度器谁更急。非结构化任务创建时可以指定,比如 Task(priority: .high) { ... }。当父任务优先级提升,它的子任务也会跟着升,保证重要工作先被照顾。

另外,任务本地值(task-local values)能在父子任务间自动传播,让你不必把一些上下文(如请求 ID、用户令牌)逐层当参数传递。它是结构化并发层级关系的又一项红利——只要是同一棵任务树,值自动可达。

Note

优先级只是”建议”,不是硬保证。真正决定快慢的还有系统资源、任务是否阻塞等。别把优先级当成精确调度开关,它更像”大致倾向”。

1-9 小结

Task 是并发的执行单位,彼此构成父子层级。从同步代码起步用 Task {},需要彻底独立用 Task.detached {},拿到句柄后可 await 结果或 cancel()。取消是协作式的:用 Task.checkCancellation() 抛错式停下,或用 Task.isCancelled 做带清理的停下,且取消自动沿任务树向下传播。

理解 Task 与取消,是驾驭并发的枢纽。下一章进入 actor,看 Swift 怎么用”轮流访问”来保护共享的可变数据、根除数据竞争。