结构化并发是 Swift 5.5+ 引入的核心并发模型,简单来说:它让异步任务的生命周期与代码的语法结构严格绑定,父任务能管控子任务的创建、执行和销毁,避免"游离任务"导致的内存泄漏、崩溃或资源泄漏。
对比传统的"非结构化并发"(如 GCD 分散的 async 调用、闭包回调),结构化并发就像"有组织的团队协作"------父任务是管理者,子任务是团队成员,成员的工作范围、生命周期完全由管理者掌控;而非结构化并发则是"散兵游勇",任务创建后脱离管控,容易出现混乱。
一、核心特征(结构化 vs 非结构化)
1. 非结构化并发的问题(GCD/闭包)
swift
// 非结构化:任务创建后脱离管控,父函数返回后子任务仍可能执行
func unstructuredTask() {
// 子任务1:游离在外,无法被父函数管控
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 10) // 长时间执行
print("子任务1执行(可能父函数已返回)")
}
// 子任务2:独立于子任务1,无统一生命周期
DispatchQueue.global().async {
print("子任务2执行(无法保证与子任务1的顺序)")
}
}
// 调用后父函数立即返回,子任务仍在后台执行,易导致资源泄漏/数据竞争
unstructuredTask()
2. 结构化并发的核心规则
结构化并发遵循 3 个核心原则,确保任务"可管可控":
- 父子关联 :子任务必须由父任务创建(如
TaskGroup),父任务会等待所有子任务完成后才结束; - 作用域绑定 :任务的生命周期被限制在明确的代码块内(如
withTaskGroup的闭包),超出作用域自动终止; - 统一取消:取消父任务时,所有子任务会被自动取消,避免"僵尸任务";
- 清晰的层级:任务的执行顺序、依赖关系通过代码结构体现,而非隐藏在闭包/回调中。
二、Swift 中结构化并发的核心实现
1. 基础:Task 的结构化使用
单个 Task 本身可以是"结构化"的------当 Task 被创建在另一个异步上下文中时,它会成为父任务的子任务,继承生命周期:
swift
func structuredSingleTask() async {
// 子任务:绑定到当前异步上下文(父任务)
let childTask = Task {
await Task.sleep(1_000_000_000)
print("子任务执行(父任务未完成前不会游离)")
}
// 父任务等待子任务完成(结构化的核心体现)
await childTask.value
print("父任务完成(子任务已执行完毕)")
}
// 调用:父任务的生命周期管控子任务
Task {
await structuredSingleTask()
}
2. 核心:TaskGroup(多任务结构化管理)
TaskGroup 是结构化并发的核心工具,用于管理一组子任务,确保所有子任务完成后父任务才继续执行:
swift
/// 结构化并发:批量下载图片(TaskGroup 管控所有子任务)
func downloadImagesStructured(urls: [String]) async -> [Data] {
// withTaskGroup 定义任务组的作用域,所有子任务绑定在此作用域内
return await withTaskGroup(of: Data.self) { group in
// 1. 提交所有子任务(子任务属于该任务组)
for url in urls {
group.addTask {
// 模拟图片下载
await Task.sleep(500_000_000)
return "Image data for \(url)".data(using: .utf8)!
}
}
// 2. 收集子任务结果(父任务会等待所有子任务完成)
var images = [Data]()
for await imageData in group {
images.append(imageData)
}
// 3. 作用域结束:所有子任务已完成,无游离任务
return images
}
}
// 调用:结构化执行,无游离任务
Task {
let urls = ["url1", "url2", "url3"]
let images = await downloadImagesStructured(urls: urls)
print("所有图片下载完成:\(images.count)张")
}
3. 进阶:取消与异常的结构化处理
结构化并发的关键优势是统一的取消和异常处理------父任务可取消所有子任务,子任务抛出的异常会向上传递:
swift
func structuredCancellation() async {
await withTaskGroup(of: Void.self) { group in
// 子任务1:长时间执行
group.addTask {
for i in 1...10 {
// 检查是否被取消(结构化取消的核心)
try Task.checkCancellation()
await Task.sleep(100_000_000)
print("子任务1执行第\(i)次")
}
}
// 子任务2:触发取消
group.addTask {
await Task.sleep(300_000_000)
group.cancelAll() // 取消所有子任务
print("子任务2:取消所有任务")
}
// 等待所有子任务(包括被取消的)
await group.waitForAll()
}
print("任务组执行完毕(所有子任务已终止)")
}
// 调用:取消后子任务1会停止,无资源泄漏
Task {
await structuredCancellation()
}
三、结构化并发的核心价值
1. 避免"僵尸任务"
非结构化并发中,父函数返回后子任务可能仍在执行(如 GCD 的 async),导致:
- 内存泄漏(子任务持有外部对象);
- 资源浪费(无效的网络请求/计算);
- 崩溃(子任务修改已释放的对象)。
结构化并发中,任务生命周期绑定到代码结构,超出作用域自动终止,彻底解决此问题。
2. 简化错误处理
子任务抛出的异常会自动向上传递到父任务,无需在闭包中分散处理错误:
swift
func structuredErrorHandling() async throws {
try await withThrowingTaskGroup(of: Data.self) { group in
group.addTask {
throw NSError(domain: "DownloadError", code: -1) // 子任务抛异常
}
// 异常会自动向上传递,父任务可统一捕获
for try await _ in group {}
}
}
// 统一捕获错误
Task {
do {
try await structuredErrorHandling()
} catch {
print("捕获异常:\(error)")
}
}
3. 提升代码可读性
结构化并发让异步任务的依赖、顺序通过代码缩进/结构体现,而非隐藏在闭包嵌套中:
- 非结构化:回调嵌套(回调地狱),任务关系不直观;
- 结构化:线性代码,任务的父子、顺序关系一目了然。
四、结构化 vs 非结构化任务的对比
| 特性 | 结构化并发(TaskGroup/结构化Task) | 非结构化并发(GCD/非结构化Task) |
|---|---|---|
| 任务生命周期 | 绑定代码结构,可管控 | 游离在外,无统一管控 |
| 取消机制 | 父任务可取消所有子任务 | 需手动管理取消标记 |
| 错误处理 | 异常自动向上传递,统一捕获 | 错误分散在闭包中,易遗漏 |
| 资源泄漏 | 几乎无(作用域结束自动清理) | 高(任务持有外部对象) |
| 代码可读性 | 高(线性结构) | 低(回调嵌套) |
总结
- 结构化并发的核心是任务生命周期与代码结构绑定,父任务管控子任务的创建、执行、取消,避免游离任务;
- Swift 中通过
TaskGroup(多任务)、结构化Task(单任务)实现结构化并发,替代传统的 GCD 非结构化调用; - 核心价值:解决非结构化并发的资源泄漏、错误处理混乱问题,同时提升代码可读性和可维护性;
- 适用场景:所有需要管理多个异步任务的场景(如批量网络请求、并行计算),是 Swift 并发模型的核心推荐范式。