结构化并发:async let 与 TaskGroup
本教程共 93 篇 · 第 81 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:用 async let 并行跑少量已知子任务,用任务组动态并行一批子任务并收集结果,理解结构化并发的父子生命周期保障。
上一章你学会了顺序地 await 一个异步函数。可有时候三件事互不依赖,完全可以同时跑、最后一起收结果。await 一个接一个等太慢了。Swift 的结构化并发就是干这个的:用 async let 和任务组(TaskGroup)把子任务组织起来,既并行又可控。
1-1 async let:并行启动已知子任务
普通 await 调用会等结果回来才往下走,子任务是串行的:
let first = await downloadPhoto(named: names[0])
let second = await downloadPhoto(named: names[1])
let third = await downloadPhoto(named: names[2])
let photos = [first, second, third]
三段下载一个接一个,总耗时是三者之和。但它们互不相干,完全能同时下。把 let 前面加上 async,就变成并行启动:
async let first = downloadPhoto(named: names[0])
async let second = downloadPhoto(named: names[1])
async let third = downloadPhoto(named: names[2])
let photos = await [first, second, third]
async let 那三行不写 await,所以代码不挂起,三个下载同时出发。直到 await [first, second, third] 这一行,程序才需要结果,于是停下等它们全部完成。总耗时约等于最慢的那一个,而不是三者之和。
Tip判断用
await还是async let的简单标准:后面的代码依赖这个结果吗?依赖就用await(顺序);不依赖、只想稍后一起收,就用async let(并行)。
1-2 async let 的两种典型用法
async let 适合”子任务数量和它们各自的逻辑在写代码时就确定了”的情况。比如同时拉取用户资料、好友列表、动态三条数据:
func loadDashboard() async {
async let profile = fetchProfile()
async let friends = fetchFriends()
async let feed = fetchFeed()
let (p, f, d) = await (profile, friends, feed)
print("资料:\(p),好友数:\(f.count),动态:\(d.count)")
}
这里三个 async let 并行启动,最后一次性 await 把三个结果打包收回来。代码读起来依旧是线性、清爽的,没有回调套回调。
注意 await 可以作用在多个 async let 组成的元组上,一次性等齐。这是 async let 常见的收尾姿势。
1-3 任务组:动态数量的并行
async let 的短板是数量写死了。如果要下载的照片有几张、几十张,数量运行时才定,就得用任务组。withTaskGroup 让你显式建一个组,往里动态加子任务:
func downloadAll() async {
await withTaskGroup(of: Data.self) { group in
let names = await listPhotos(inGallery: "暑假")
for name in names {
group.addTask {
return await downloadPhoto(named: name)
}
}
for await photo in group {
show(photo) // 每下载完一张就展示一张
}
}
}
withTaskGroup(of:) 的 of 指定每个子任务返回的类型。循环里 addTask 往组里加子任务,它们并发执行。下面的 for await photo in group 按完成顺序逐个取出结果——注意不保证顺序,谁先好谁先来。
Note任务组是”结构化”的核心体现:组的代码块结束前,Swift 保证所有子任务都跑完;父任务被取消,子任务也自动取消。你不会忘记等子任务,也不会漏掉取消,这正是结构化并发比手写线程安全的地方。
1-4 任务组收集结果到数组
上面的例子边收边展示,没存结果。如果想把全部结果攒成一个数组返回,就在循环里 append:
let photos = await withTaskGroup(of: Data.self) { group in
let names = await listPhotos(inGallery: "暑假")
for name in names {
group.addTask {
return await downloadPhoto(named: name)
}
}
var results: [Data] = []
for await photo in group {
results.append(photo)
}
return results
}
for await 每拿到一个子任务的结果就 append 进数组,等所有子任务结束,把数组作为整个任务组的返回值交出去。withTaskGroup 的返回值就是闭包里 return 的那个值。
1-5 会抛错的任务用 throwing 变体
如果子任务可能抛错,用 withThrowingTaskGroup:
func downloadAllSafe() async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
let names = await listPhotos(inGallery: "暑假")
for name in names {
group.addTask {
return try await downloadPhotoThrowing(named: name)
}
}
var results: [Data] = []
for try await photo in group {
results.append(photo)
}
return results
}
}
注意区别:调用处多一个 try,循环里是 for try await。只要有一个子任务抛错,整个任务组会抛出,父任务能 do-catch 捕获。这是并发代码里处理错误的标准姿势。
1-6 结构化并发的四项保障
为什么 Swift 力推这种”结构化”写法,而不是让你随便开线程?因为父任务与子任务有明确的层级关系,带来四个实在好处:
第一,父任务不会漏等子任务——块结束前子任务必须全部完成。
第二,给子任务提了更高优先级,父任务的优先级会自动跟着提升。
第三,父任务被取消,所有子任务也自动取消,不会留下孤儿任务。
第四,任务本地值(task-local)能高效自动地传给子任务。
Tip记住一句话:
async let是”隐式”创建的子任务,适合数量固定的场景;withTaskGroup是”显式”创建并动态管理子任务,适合数量不定、需要更细控制(优先级、取消)的场景。两者都是结构化并发,都享受上面的保障。
1-7 async let 与任务组能混用
两种方式不是二选一,可以组合。比如先用 async let 拉取名称列表,再进任务组批量下载:
func buildGallery() async throws {
async let names = listPhotos(inGallery: "暑假")
let photos = try await withThrowingTaskGroup(of: Data.self) { group in
for name in await names {
group.addTask { try await downloadPhotoThrowing(named: name) }
}
var r: [Data] = []
for try await p in group { r.append(p) }
return r
}
print("共下载 \(photos.count) 张")
}
这里 async let names 先并行取列表,取回后喂给任务组去并发下载。两种结构在同一段逻辑里协作,是真实代码里常见的形态。
1-8 优先级与取消在任务组里怎么用
任务组不只是”并行”,还能精细控制。给子任务提优先级、在取消后不再加新任务,都是常见刚需。
addTask 可以带 priority 参数,告诉系统这个子任务多紧急;如果父任务优先级被提升,子任务也会跟着升。取消方面,与其盲目 addTask,不如用 addTaskUnlessCancelled:一旦组已被取消,它直接返回 false、不再启动新工作。
await withTaskGroup(of: Data?.self) { group in
for name in names {
let added = group.addTaskUnlessCancelled {
Task.isCancelled ? nil : await downloadPhoto(named: name)
}
guard added else { break } // 已取消就停止添加
// ...
}
}
这样取消信号一来,既不再开新任务,已有的任务也在下次检查 isCancelled 时干净退出,已经下完的部分结果还能保留返回,而不是全盘丢弃。
Tip处理”可以中途放弃、但已完成的成果要留住”的场景,
addTaskUnlessCancelled+ 返回可选结果 是标准套路。它体现了结构化并发”取消但不浪费已完成工作”的体贴。
1-9 性能直觉:并行不是白来的
并行能加速,但也不是”拆得越细越好”。每个子任务都有调度开销,如果一件事本身只要几毫秒,硬拆成几百个子任务,调度成本反而可能拖慢整体。
经验上,只有当单个子任务确实耗时(比如网络请求、大文件解析、复杂计算),并行才有明显收益。对一堆极轻的操作,async let/任务组带来的并行收益,可能被创建和协调任务的开销吃掉。所以”要不要并行”首先看任务够不够重,而不是看语法炫不炫。
Note系统能同时跑多少个子任务,取决于机器的核心数和当前负载。任务组会自己权衡”同时跑几个”,你不必手动指定并发上限——这也是结构化并发省心的地方。
1-10 小结
并行化异步工作靠结构化并发:async let 适合数量已知的少量子任务,写完就并行、最后 await 一起收;withTaskGroup 适合数量运行期才定的批量子任务,能动态添加并逐个收集结果,抛错用 withThrowingTaskGroup。结构化并发保证父等子、父子同取消,把并发安全内置进语法。
下一章深入 Task 本身:任务树、取消机制,以及非结构化任务怎么用。