深入 Swift 结构化并发
Explore structured concurrency in Swift
2021年6月7日
一句话判断
如果你想理解 Swift Concurrency 为什么比 GCD 更安全、为什么 async/await 不是”语法层面的进步”而是”语义层面的飞跃”,这场 Session 是必看材料——结构化并发(Structured Concurrency)是整个设计的精神内核。
这场 Session 讲了什么
Session 从”结构化编程”的历史讲起:当年 goto 被淘汰,是因为它破坏了代码的控制流结构。同样地,非结构化的并发(手动创建线程、散落的 GCD 调用)也在破坏并发的结构。Swift 5.5 引入的结构化并发就是要解决这个问题。
核心概念是”任务树”(Task Tree)。一个父任务创建的子任务,其生命周期被父任务管理:父任务必须等所有子任务完成才能结束,子任务的错误会向上传播给父任务,取消操作从父到子级联生效。async let 和 TaskGroup 是两种创建结构化并发的手段。前者适合固定数量的并发操作,后者适合动态数量。Session 还详细讲了 Task 的取消机制——不是强制中断,而是协作式取消,通过 Task.isCancelled 和 Task.cancel() 配合使用。
值得深挖的点
TaskGroup 的 reduce 和收集策略
TaskGroup 提供了两种收集子任务结果的方式:直接 for await 遍历,或用 reduce 方法。关键细节是子任务的完成顺序是不确定的——你添加子任务的顺序不代表它们完成的顺序。for await 会按完成顺序产出结果。如果你需要按添加顺序收集结果,需要子任务自己返回带索引的结果类型。这个设计选择是有意为之的:不引入额外的排序开销。
withTaskGroup 的错误传播语义
withTaskGroup 的闭包中,如果任何一个子任务抛出错误,group 会在所有已添加的子任务完成后将错误传播给调用者。但这里有个微妙的点:即使某个子任务抛错,其他已经启动的子任务仍然会执行完毕(只是结果会被丢弃)。如果你希望在第一个错误发生时就取消所有其他子任务,需要手动检查 Task.isCancelled 或者使用 withThrowingTaskGroup 配合显式的取消逻辑。
代码片段
// async let:固定数量的并发操作
// 三个请求同时发出,全部完成后一起处理
func loadDashboard() async throws -> Dashboard {
// 三个网络请求并行执行
async let userProfile = fetchUserProfile()
async let feedItems = fetchFeed()
async let notifications = fetchNotifications()
// 等待所有结果,任何一个失败都会取消其他请求
return try await Dashboard(
profile: userProfile,
feed: feedItems,
alerts: notifications
)
}
// TaskGroup:动态数量的并发操作
// 并行下载多张图片,按完成顺序收集
func downloadImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
// 为每个 URL 创建一个子任务
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
// 按完成顺序收集结果
var images: [UIImage] = []
for try await image in group {
images.append(image)
}
return images
}
}
// 协作式取消:在长时间任务中检查取消状态
func processLargeDataset(_ data: [Record]) async throws -> [Result] {
var results: [Result] = []
for (index, record) in data.enumerated() {
// 定期检查是否被取消
if Task.isCancelled {
print("处理被取消,已处理 \(index) 条记录")
return results // 返回已完成的部分结果
}
let result = try await processRecord(record)
results.append(result)
}
return results
}
最佳实践
- 优先用
async let而不是TaskGroup,前者语法更简洁,编译器检查更严格。只在子任务数量动态变化时才用TaskGroup。 - 长时间运行的异步操作务必实现取消检查——每处理一个数据批次检查一次
Task.isCancelled,这是结构化并发的”礼貌”。 - 不要在结构化并发的上下文中使用
Task.detached——它脱离了任务树,取消不会级联传播,错误不会被捕获。除非你有非常明确的理由(比如一个真正独立的后台计算任务)。
还有什么值得关注
Task.priority可以在创建时指定优先级(.high、.medium、.low、.background),Swift 运行时会据此调度执行顺序。TaskLocal值可以在任务树中从父到子传递上下文信息(比如 trace ID),非常适合做分布式追踪。- Xcode 13 的 Instruments 新增了 Swift Concurrency 模板,可以可视化任务创建、挂起和完成的时间线。