Concurrency系列还剩最后一个问题。到目前为止,我们看过async函数(第1篇)、actor(第2篇)和Sendable(第3篇)。
本文接续上一篇文章 Swift进阶 #3。
我们还没有真正处理进入异步世界的入口Task。而一旦处理Task,这个系列中最实用的概念就会出现。
结构化并发。
名字听起来很宏大,但问题很简单:启动异步工作后,谁负责它的结束?
“结构化”的含义 — 工作也有作用域
这个名字来自结构化编程这一旧术语。经历了goto可以把控制流跳到任意位置的时代后,if和for等代码块结构开始保证“控制流会返回进入的位置”。
结构化并发将同一原则应用于异步工作。子任务不能离开父作用域,父任务必须等所有子任务完成后才能结束。
第1篇介绍的async let,其实就是这一原则的第一个例子。
func loadDashboard() async throws -> Dashboard {
async let profile = fetchProfile() // 子任务 1
async let feed = fetchFeed() // 子任务 2
return try await Dashboard(profile: profile, feed: feed)
} // 这个函数在两个子任务完成清理前无法返回
使用async let创建的子任务必须在函数返回前完成或被取消。发生异常时也一样。
如果feed抛出错误,仍在运行的profile会自动收到取消信号。函数完成清理后才会传播错误。
由于工作绑定在作用域中,因此结构上不可能存在“启动后就忘掉的任务”。
在闭包篇中,escaping是“生命周期比函数更长的闭包”的警告标签。结构化并发则从默认模型中彻底移除了“生命周期比函数更长的工作”。
如果子任务数量是动态决定的,就使用TaskGroup。并行获取URL列表的典型代码如下。
let images = try await withThrowingTaskGroup(of: (URL, Image).self) { group in
for url in urls {
group.addTask { (url, try await download(url)) }
}
var result: [URL: Image] = [:]
for try await (url, image) in group {
result[url] = image
}
return result
}
有两点值得注意。结果按完成顺序到达(如果需要保证顺序,就像上面一样和键一起保存),并且闭包结束时,组中的所有子任务都会完成清理。
如果说async let是“几个子任务”的语法,那么TaskGroup就是“n个子任务”的语法,作用域保证相同。
取消 — 信号会到,但停止是你的责任
结构化并发的第二个支柱是取消传播。父任务被取消后,取消信号会沿树向下传给所有后代。
离开页面时,该页面发起的网络请求会连锁取消,正是得益于这种结构。
不过Swift的取消是协作式(cooperative)的。取消信号不会强制杀死任务。
它只会设置isCancelled标志;检查标志并停止,是任务自己的责任。
func processLargeFile() async throws {
for chunk in chunks {
try Task.checkCancellation() // 如果已取消,就抛出 CancellationError
await process(chunk)
}
}
为什么不是强制终止?因为任务如果在文件写到一半、持有锁,或处于事务中途时突然死亡,系统可能会留下损坏状态。
协作式取消的逻辑是:“只有任务自己知道在哪里停止最合适。”
实际含义很明确:长时间运行的循环必须加入checkCancellation。
URLSession等系统API已经在内部遵守取消,因此可以直接信任它们。
相反,忽略取消的长时间计算代码即使被取消也不会停止。不是取消失效,而是没有进行检查。
Task与Task.detached — 非结构化世界及其代价
如果到这里都是结构化世界,那么Task { }就在其外部。使用Task创建的任务不属于父子树,是非结构化(unstructured)任务。
它不受作用域约束,错误和取消也不会自动传播。
那它为什么存在?因为我们需要一座从同步世界通往异步世界的桥。
按钮点击处理程序是同步函数,无法使用await,因此Task { await viewModel.refresh() }会打开新的异步上下文。这就是Task合理的使用场景。
SwiftUI的.task修饰符在这座桥上加入了生命周期管理(视图消失时自动取消),所以UI中应优先使用它。
问题在于Task变成习惯。在async函数中再次创建Task,或以fire-and-forget方式丢出的Task,都是例子。结构提供的保证(等待完成、错误传播、取消链)全都需要手动补回。
你必须保存引用、手动调用cancel,也要手动记录错误。没有这些管理代码,就会出现悄悄消失的错误和僵尸任务。
归纳成规则:在async上下文中,默认使用async let和TaskGroup;Task只用于同步→异步边界。
Task.detached更进一步脱离结构。它不继承优先级、actor隔离或task-local值,是完全孤立的任务(SE-0304)。
人们常因“想离开@MainActor上下文执行繁重工作”而使用它,但大多数情况下这是错误的做法。将函数声明为nonisolated async,它就会自动在协作线程池中运行(第1篇)。
真正需要detached的情况很少,例如必须刻意独立于当前上下文的后台工作。借用官方文档的说法,它是最后手段(last resort)。
系列总结 — 四个概念构成的一幅图
在结束Concurrency系列时,让我们一次性整理完整全貌。
async/await让异步流程重新回到编译器的视野中(第1篇)。actor为共享可变状态内置了串行保护(第2篇),Sendable则检查跨越隔离边界的值是否安全(第3篇)。
而结构化并发将任务生命周期绑定到作用域。开始的工作必须结束,取消会沿树流动(本篇)。
贯穿四者的一句话是:把并发的隐式规则变成语言的显式结构。
线程管理规则变成了协作线程池和挂起,锁规则变成了actor。“这个对象能否传到其他线程”的口头知识变成了Sendable,“别忘了清理请求”的评审意见变成了任务树。
Swift哲学系列第1篇中的安全优先原则,已经扩展到并发这一最困难的领域。这就是Swift Concurrency的完整故事。
总结
- 结构化并发的原则是“子任务不能离开父作用域”。async let(固定数量)和TaskGroup(动态数量)是对应语法,出错时兄弟任务取消和清理都会自动完成。
- 取消是协作式的。信号会沿树传播,但会停止的是检查checkCancellation的任务本身。
- Task { }只用于同步→异步桥接。在async上下文中习惯性使用Task,会把结构保证变成手动管理负债。Task.detached是最后手段。
- 整个系列的要点:线程、锁和生命周期管理的隐式规则,已经转移为挂起、actor、Sendable和任务树等语言结构。
从下一篇开始,我们将深入性能:final为什么是性能关键字、协议调用在哪里变慢,以及静态分派和动态分派的真实机制。
延伸阅读
来源与验证
- SE-0304: Structured ConcurrencySwift Evolution · 标准或规范 · 核查 2026年8月17日依据: Task与TaskGroup的父子结构;取消、优先级、task-local和actor上下文继承

![[Swift进阶 #4] 结构化并发:为什么不能随意创建Task 封面图](/assets/images/posts/ab72c2de-7b69-438f-82b8-c4c8ba431871/swift-structured-concurrency-1.jpg)