引言:为什么需要新的并发模型?
在传统 iOS/macOS 开发中,我们使用 GCD(Grand Central Dispatch)或 OperationQueue 来处理并发任务。然而,这些技术存在一些痛点:
- 回调地狱:多层嵌套的回调难以阅读和维护
- 手动内存管理:容易忘记 weak self 导致内存泄漏
- 线程爆炸:过度创建线程消耗系统资源
- 数据竞争:共享状态需要手动加锁,容易出错
Swift 5.5 引入的 async/await 和结构化并发解决了这些问题,提供了更安全、更简洁的并发编程方式,iOS 13以上是支持的。
AI很多是错误的,因此下面是经过自己Xcode 26.5手动尝试的总结,作为一个手册方便后续查看。
整体架构
类结构图,先对整体角色职责有个了解:
一、async/await 基础语法
1.1 异步函数声明
swift
// 1.传统回调方式
func fetchUser(completion: @escaping (Result<User, Error>) -> Void)
// 2.异步函数方式, 最新Swift语法:async必须在throws之前
func fetchUser() async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return User(data) // 调用者负责处理错误
}
1.2 异步函数调用
swift
// 1. 调用的地方有声明async
func caller() async {
// 使用 await 调用异步函数
do {
let user = try await fetchUser()
print("用户: \(user.name)")
} catch {
print("错误: \(error)")
}
}
// 2.如果调用的函数本身无async修饰,则需要使用Task调用
// 普通函数没有"暂停-恢复"能力,所以必须通过 `Task` 创建**新的异步执行上下文**。
func testCallWithNoAsync() {
Task {
do {
let data = try await fetchData()
print("Received \(data.count) bytes")
} catch {
print("Failed: \(error)")
}
}
}
// 3.异步闭包
let handler: @Sendable () async -> Void = {
try? await Task.sleep(nanoseconds:1_000_000_000)
}
// 4.使用 @escaping 的异步闭包
func perform(operation: @escaping () async -> Void) async {
await operation()
}
异步属性
swift
// 异步属性
var thumbnail: Data {
get async throws {
try await Task.sleep(nanoseconds: 1)
return Data()
}
}
func testProperty() async {
do {
let thumbnail = try await self.thumbnail
} catch {
print("获取属性时异常\(error)")
}
}
1.3 async let并发启动多个任务
swift
// 同时启动多个异步任务
func loadDashboard() async throws -> Dashboard {
async let user = fetchUser() // 子任务1立即开始
async let orders = fetchOrders() // 子任务2立即开始
async let messages = fetchMessages() // 子任务3立即开始
// 等待所有任务完成
return try await Dashboard(
user: user,
orders: orders,
messages: messages
)
}
当父任务退出作用域时,编译器(或者说运行时)会发现还有两个未完成的 async let 子任务。根据结构化并发的规则:
- 隐式取消 :运行时会自动向
child1和child2发送取消信号。 - 隐式等待 :父任务不会立即返回 ,而是隐式地
await这两个子任务直到它们响应取消并结束。 - 抛出错误 :如果
longWork()和anotherWork()内部有检查取消状态(如try Task.checkCancellation()),它们会抛出 CancellationError,这个错误会被该作用域外层的catch捕获。
swift
// 后面没有await的话,子任务会被取消
Task {
async let child1 = longWork() // 启动子任务 1
async let child2 = anotherWork() // 启动子任务 2
// ⚠️ 直接退出作用域,没有 await child1 和 child2
}
1.4 async/await执行对比
总耗时:
swift
// 并发执行(总耗时 ≈ 最慢的任务)
async let a = taskA() // 0-1秒
async let b = taskB() // 0-2秒
let results = await (a, b) // 总耗时: 2秒
// 顺序执行(总耗时 = 所有任务时间之和)
let a = await taskA() // 0-1秒
let b = await taskB() // 1-3秒(等A完成后才开始)
// 总耗时: 3秒
异常:
Swift
// 串行:遇到第一个错误立即停止
do {
let a = try await fetchA() // 如果失败,不会执行 fetchB
let b = try await fetchB()
} catch {
// 只捕获第一个错误
}
// 并行:所有任务完成后再处理错误
do {
async let a = fetchA() // 立即开始
async let b = fetchB() // 立即开始
let (resultA, resultB) = try await (a, b)
} catch {
// 可能捕获 a 或 b 的错误,但两者都可能已执行
}
1.5 本质
| 维度 | 说明 |
|---|---|
| 编译期变换 | 编译器将 async 函数编译为状态机 (Coroutines),每个 await 是一个挂起点(suspend point) ,函数在此拆分为多个续延段(continuation) |
| 线程不阻塞 | await 挂起时释放线程 (而非阻塞线程),线程可去执行其他任务;当结果就绪,运行时在合适线程恢复执行 |
| 调用栈保存 | 挂起时,Swift 运行时将当前栈帧保存到堆上;恢复时重建栈帧,对外表现为"同步顺序代码" |
| 与 Coroutine 的关系 | Swift 的 async/await 本质是 Stackless Coroutine(无栈协程),对比 Kotlin/Python 的有栈实现,内存开销更小但无法在任意位置挂起 |
1.6 使用注意点
await不是线程切换 :await只表示"可能挂起",恢复时不保证回到同一线程- 不要在
deinit中使用async:deinit是同步的,对象可能已释放 async let的生命周期 :必须在作用域结束前await,否则任务取消但不会报错- 重入问题 :
actor的async方法在await挂起后恢复时,隔离状态可能已变,这是 actor 重入的根源 - 死锁风险 :同步函数中无法调用
async函数,必须通过Task包裹;但Task在MainActor上可能造成主线程等待
1.7 相关疑问
Q: async let user = fetchUser() 立即返回什么?
A: 它不立即返回数据,而是返回一个异步任务句柄 。实际数据在
await时获取。
Q: 多个 async let 相当于 GCD 的异步任务吗?
A: 是不同概念。
async let是结构化并发的一部分,在离开作用域前自动等待所有子任务完成;而GCD异步任务只是分发出去。
Q: await时对应的任务在哪个线程执行?
- 执行对应任务的线程是系统调度,不确定哪个线程。
await挂起后恢复时,不保证在同一个线程 ,但保证在同一个执行器(Executor) 的上下文中。
swift
// 情况一:
Task {
print("当前线程: \(Thread.current)") // Thread 1
await something()
print("恢复后线程: \(Thread.current)") // 可能 Thread 1,也可能 Thread 2
}
// 情况二:
@MainActor
class ViewModel {
func loadData() async {
print("主线程: \(Thread.current.isMainThread)") // true
await Task.sleep(nanoseconds: 1_000_000_000)
print("恢复后: \(Thread.current.isMainThread)") // 仍然是 true
// 虽然可能切换线程,但仍然在主线程上下文中
}
}
二、Actor:数据隔离类型
Actor 本质上是"自带串行执行器的引用类型" ,编译器在其基础上自动实现数据隔离 和任务调度 。 从实现角度看,actor 类似于一个特殊的 class:
- 它拥有类的全部能力:继承、存储属性、方法、下标、初始化器、析构器。
- 但编译器会为每个 actor 实例自动合成一个串行执行器(executor) ,所有对该 actor 状态(存储属性、方法调用)的访问都必须通过这个执行器串行化。
- 与 GCD 串行队列的关键区别在于:Actor 执行器是可重入(reentrant)的。
2.1 actor基本使用
Swift
// 一、修饰类型定义,相当于类
// 内部访问无async修饰的属性和方法无须使用await
// 内部访问async的属性或方法仍然要await
actor BankAccount {
private var balance: Double = 0
let id: String = "" // let 属性天然线程安全
func deposit(amount: Double) {
balance += amount // 安全:在同一 actor 内
}
func getBalance() -> Double {
return balance// 同 Actor 内部同步访问无需await
}
// 1. let 属性天然线程安全
// 2. nonisolated可以修饰let的属性;不能修饰var属性会报错
let id: String = ""
// 3) nonisolated --- 脱离隔离方法
nonisolated func description() -> String {
"Account(\(id))" // 不需要 await,可同步调用
}
}
func testActor() {
// 2) 使用
let account = BankAccount()
Task {
await account.deposit(amount: 10) // 跨 Actor 必须 await
await print(account.getBalance())
}
}
// 二、跨 actor 调用需要 await
actor ActorA {
func doSomething() { }
}
actor ActorB {
let a = ActorA()
func test() async {
await a.doSomething() // 需要 await
}
}
2.2 isolated隔离特性
nonisolated修饰的属性及方法:
- nonisolated可以修饰let的属性;不能修饰var属性会报错。
- 外部调用nonisolated方法无须await
- nonisolated函数内部访问非nonisolated函数或属性会报错;允许访问let属性
isolated 修饰actor类型的参数:
- 只能有一个参数被
isolated修饰 - 修饰之后函数将运行在该参数所属的隔离上下文中,所以该参数的访问无须await
Swift
actor BankAccount {
var name = ""
// 1. let 属性天然线程安全,不加nonisolated也是可以同步访问的
// 2. nonisolated可以修饰let的属性;不能修饰var属性会报错
nonisolated let id: String = ""
// 3. nonisolated --- 脱离隔离方法
//
nonisolated func description() -> String {
// 对于let属性访问不需要 await,可同步调用
"Account(\(id))"
// 访问非nonisolated函数或属性会报错
// Actor-isolated property 'balance' can not be referenced from a nonisolated context
"Account(\(name))"
}
// isolated 隔离某个 actor参数
func transfer(from: isolated BankAccount, to: BankAccount, amount: Double) async {
from.withdraw(amount) // 无需 await,因为 from 被隔离
await to.deposit(amount)
}
}
// 外部调用nonisolated方法无须await
2.3 自定义全局 Actor
在纯 Swift 标准库层面, @MainActor 是唯一内置的全局 Actor。
@MainActor:绑定到主线程,用于 UI 安全和主线程串行化。- 自定义全局 Actor :开发者可以通过 conforming to
GlobalActor协议来创建自己的全局单例 Actor,用于管理特定的资源或线程池(例如数据库访问队列、网络请求队列等)。
actor 和 @MainActor 已经提供了数据隔离,但 @globalActor 解决的是跨多个类型共享同一个隔离上下文 的需求。如下面的@DatabaseActor隔离可以修饰多个类型。
Swift
// GlobalActor 协议要求 shared 属性返回的类型必须是一个 Actor(或者满足特定条件的其他类型)。
// 下面示例不能使用struct,会报错。
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
private let queue = DispatchQueue(label: "db")
// rethrows:智能判断函数仅当参数闭包work抛出错误时才抛出错误
public func execute<T>(_ work: () throws -> T) rethrows -> T {
try queue.sync { try work() }
}
}
@DatabaseActor
class DatabaseManager {
// 所有方法自动在 DatabaseActor 上执行
// func query() -> [Record] { ... }
}
@DatabaseActor
class UserRepository {
// 所有方法和属性自动在 DatabaseActor.shared 上执行
// func fetch() -> User { ... } // 无需写 await
}
2.4 @MainActor - 主线程隔离
@MainActor 可以用于以下位置"
| 位置 | 示例 | 效果 |
|---|---|---|
| 类/结构体/枚举 | @MainActor class ViewModel { } |
所有成员默认在主线程执行 |
| 函数/方法 | @MainActor func updateUI() { } |
该函数必须在主线程调用 |
| 属性 | @MainActor var title: String { get set } |
getter/setter 在主线程 |
| 闭包 | Task { @MainActor in ... } |
闭包体在主线程执行 |
| 全局变量/常量 | @MainActor var shared = Data() |
访问该变量需要在主线程 |
| 扩展 | @MainActor extension ViewModel: { } |
整个扩展遵循 MainActor |
在无法使用 @MainActor 标记(例如你需要留在后台线程,只是临时更新 UI)时使用:MainActor.run
swift
// 在任何线程
func example() async {
// 当前任务(可能在后台线程)
// 方式1:Task { @MainActor in }
let task = Task { @MainActor in
}
await task.value
// 方式2:MainActor.run
await MainActor.run {
// 不是新任务,仍然是当前任务的执行片段
// 当前任务被取消时,这个闭包也会被取消(因为属于同一个任务)
}
}
- **行为**:将闭包提交到主线程执行器,当前任务可能挂起,等待主线程空闲后执行闭包。
- **异步性**:`MainActor.run` 是一个 `async` 函数,调用时需要 `await`。
2.5 Actor 重入(Reentrancy)
Actor 可以在await任务挂起点让出执行权,允许其他任务进入执行,这就是重入。
- 当前任务被挂起 (suspend),并暂时释放 Actor 的执行器。
- 执行器立即从队列中取出下一个任务开始执行。
- 当被挂起的任务恢复时,它会重新进入队列末尾(或按优先级插队),等待再次获得执行权。
swift
actor ImageLoader {
private var cache: [String: UIImage] = [:]
func load(_ url: String) async -> UIImage {
if let cached = cache[url] { return cached }
// 挂起点:网络请求
let data = await downloadImage(url) // ← 这里挂起
// 其他任务可能在此期间进入并修改 cache
// 所以不能假设之前的 cache[url] 仍然为空
if let cached = cache[url] { return cached } // ✅ 需要二次检查
let image = UIImage(data: data)!
cache[url] = image
return image
}
}
注意:重入性要求编写幂等逻辑,避免假设状态不变。
2.6 防死锁
因为Actor的执行器是串行的但支持重入,所以内部方法调用不会死锁。
场景1:调用同一 actor 的另一个非异步方法
swift
//
actor MyActor {
func methodA() {
methodB() // 直接同步调用,无需 await
}
func methodB() {
print("ok")
}
}
methodA和methodB运行在同一个任务中,当前任务已经持有了该 actor 的执行权。- 调用
methodB只是当前任务内部的函数调用,不会排队、不会挂起,所以没有机会形成死锁。
场景2:调用同一 actor 的另一个异步方法
swift
actor MyActor {
func methodA() async {
await methodB() // 显式 await
}
func methodB() async {
print("ok")
}
}
- 当
methodA执行到await methodB()时,当前任务会挂起,并暂时释放对 actor 的执行权。 - 由于执行器是串行的,此时其他等待的任务可以进入该 actor 。但注意:
methodB()还没有开始执行,因为methodB自身也是一个需要获取 actor 执行权的任务。 - 然而,Swift 的 actor 执行器支持立即重新获取 :如果当前没有其他任务在等待,挂起后立即恢复执行
methodB,不会死锁。如果有其他任务在等待,methodB会被排到队列末尾,而当前任务仍然在等待methodB,这不会造成死锁,只会使当前任务延迟。因为没有形成循环等待------当前任务只是等待它派生的子任务完成,子任务最终会得到执行。
场景3:调用另一个 actor 的方法
swift
actor A {
let b = B()
func test() async {
await b.work() // 挂起,等待 B
}
}
actor B {
func work() async { }
}
test()在等待b.work()时会挂起,释放对 actor A 的执行权,允许其他任务进入 A。- B 的
work()独立执行,不会反过来等待 A(除非代码里有循环依赖),所以无死锁。 - 如果循环依赖(A 等 B,B 等 A),由于 actor 执行器是可重入的,也不会死锁------重入允许 B 在等待 A 时,A 中等待 B 的任务已被挂起,B 还能继续执行?实际情况会更复杂,但 Swift 官方设计保证了 actor 不会因为简单的方法调用产生死锁,因为挂起点允许执行器调度其他任务,打破了死锁的"互相持有并等待"条件。
2.7 本质
每个 Actor 实例内部都有一个串行执行器 (serial executor)。可以把它想象成一条单车道隧道:
- 所有要访问该 Actor 的任务(外部调用
await actor.method())都会进入这个队列。 - 执行器一次只允许一个任务通过隧道(执行任务代码)。
- 其他任务在隧道外排队等候,直到当前任务完全结束 或主动让出。
swift
// 示意图:Actor 的串行队列
┌───────────────────────────┐
│ Actor 的执行器队列 │
│ ┌─────┬─────┬─────┐ │
│ │ T1 │ T2 │ T3 │ ... │
│ └──╥──┴─────┴─────┘ │
│ ║ 一次只执行一个 │
│ ▼ │
│ ┌─────────────┐ │
│ │ 正在执行 T1 │ │
│ └─────────────┘ │
└───────────────────────────┘
实现机制 :Swift 运行时为每个 Actor 分配一个 UnownedSerialExecutor(可自定义)。提交任务时,任务被包装成 Job 并入队;执行器出队并执行 job.run()。
三、Task-异步任务单元
Task 是 Swift 并发中的基本执行单元,代表一个异步操作的实例。它提供了创建、管理、取消异步操作的接口。
3.1. Task基本使用
- 创建即提交 - 不需要额外的 API
- 当前线程不阻塞 - 调度到其他线程执行
- 优先级决定顺序 - 但都晚于当前同步代码
- 无法"挂起"启动 - 如果需要延迟,在{}内部自己await等待
swift
// 一、简单调用:不需要获取返回值
func test() {
// A.分发一个异步任务(内部无await函数调用也可以)
// B.创建即提交到全局执行器,没有所谓的start方法
// C.提交Task后,后续代码继续执行,无需关注Task里面细节
Task {
print("3")
}
// 方式1:标准任务(继承当前优先级、TaskLocal 和 Actor 上下文)
// 相比直接写await,Task不会阻塞后面代码执行
Task {
let data = try await fetchData()
updateUI(data)
}
}
// 二、调用Task:需要获取任务结果
func testTaskResult() async {
let task = Task {
let data = try await fetchData()
return String(data.count)
}
// 方式一:等待并获取结果
// let result = await task.value
// 方式二:带错误处理
do {
let result = try await task.value
print("带错误处理:\(result)")
} catch {
print("Task failed: \(error)")
}
}
3.2 Task的优先级
首先看到系统API会有疑问:定义优先级时为什么选择结构体而不是枚举❓
可扩展性 和向后兼容性 : 枚举无法扩展无法自定义, Apple 选择了结构体 + 静态常量的模式,在保持 API 稳定的同时,为未来扩展留下空间.
swift
// 优先级等级
// .high, .medium, .low, .userInitiated, .utility, .background
// .userInitiated ≈ high, .utility ≈ low, .background 最低
func testTaskPriority() async {
// 指定优先级
Task(priority: .high) {
await urgentWork()
}
Task(priority: .background) {
await cleanupWork()
}
}
3.3 detached:分离任务
Task.detached 用于创建一个完全独立于当前任务上下文 的新任务。它与普通 Task { } 的最大区别在于:不继承父任务的优先级、任务局部值(TaskLocal)、Actor 隔离上下文。
Task.detached { } 默认运行在 Swift 运行时的"全局并发执行器(Global Concurrent Executor)"上。 这是一个由整个运行时共享的、用于执行没有严格执行器要求的任何工作的线程池
swift
// 任务分离
func testDetached() {
// 分离任务(不继承优先级、TaskLocal、Actor)
Task.detached {
await backgroundWork()
}
if #available(iOS 18.0, *) {
let executor = globalConcurrentExecutor
Task.detached(executorPreference: executor) {
print("Task.detached -- globalConcurrentExecutor")
}
Task.detached(executorPreference: nil) {
print("""
行为:如果传入一个 TaskExecutor(例如 globalConcurrentExecutor),任务会尽量在该执行器上运行。传入 nil 与基础的 Task.detached { } 一致:不指定执行器偏好,任务将在全局并发执行器上运行,并且不会继承外层的任何执行器偏好。
主要用途:主要用于手动打破 Actor 隔离。例如,当你处于 @MainActor 的上下文中时,可以使用此 API 强制将任务派发到后台的全局并发执行器上,避免耗时操作阻塞主线程。
""")
}
} else {
// Fallback on earlier versions
}
// 同步执行的Task,直到遇到await后才开始与detached一样的方式继续执行
if #available(iOS 26.0, *) {
Task.immediateDetached {
print("""
核心行为:它的执行是同步的,不会挂起。任务会立即在当前调用者的上下文中同步启动,一直执行到它遇到第一个挂起点(第一个 await)为止。在那之后,任务才会恢复其"detached"的本质,在合适的执行器上继续执行。
与 Task.detached 的对比:Task.detached 是异步的,它将任务提交到执行器后立即返回,任务的真正开始执行会有轻微延迟;Task.immediateDetached 用于需要立即执行一些初始化工作的场景,以减少延迟。
""")
}
} else {
// Fallback on earlier versions
}
}
3.4 任务取消
- 父任务离开作用域时没有await子任务,则会自动取消子任务,见上文中
async let部分 - 下面重点列举Task的取消相关。
Swift
// 直接 await:自动传播取消
func testCancellation() async {
let task = Task {
await a() // 会收到取消信号
await b() // 也会收到取消信号
}
task.cancel()
}
// Task 包装:不自动传播
func testCancellation() async {
let task = Task {
Task { await a() } // 独立任务,不自动取消
Task.detached { await a() } // 独立任务,不自动取消
await b()
}
task.cancel() // 只取消外层 Task,内层 Task 继续执行
}
- 取消状态的判断
Swift
// 1. Task.isCancelled => 判断当前执行的上下文是否已被取消(父级已标记取消)
// 2. task.isCancelled => 判断当前task是否已被取消
// 3. try Task.checkCancellation() =》检查当前上下文是否已被取消(父级已标记取消)
func testCancel() {
let parent0 = Task {
Task.detached {// 不会被取消
async let child1 = longWork()
await child1
}
async let child2 = anotherWork()
await child2
// await (child1, child2) // 父任务等待子任务
}
parent0.cancel() // 子任务child2 会收到取消信号
print("parent0.isCancelled = \(parent0.isCancelled)")// true
print("parent0,Task.isCancelled = \(Task.isCancelled)")// false
do {
// 此时是未取消的,child2里面做这个检查是已取消的
try Task.checkCancellation()
print("parent0.checkCancellation")
} catch {
// 如果任务已取消,抛出 CancellationError
print("parent0.checkCancellation.error=\(error)")
}
}
- 取消的监听 当需要在取消时执行清理代码(如关闭文件、释放资源、发送日志),可以使用
withTaskCancellationHandler。
swift
await withTaskCancellationHandler {
// 主要工作闭包
for i in 0..<1000000 {
if Task.isCancelled { break }
process(i)
}
} onCancel: {
// 取消时调用的闭包(可同步)
print("任务被取消,执行清理")
Task { @MainActor in
cleanUpResources()
}
}
重要特性:
onCancel闭包会在任务取消时立即 被调用(可能在主工作闭包的任意执行点,甚至在工作闭包已经检查过isCancelled之后)。onCancel闭包不能挂起 (不能是async),因为它会在取消信号到来时同步执行。- 如果需要在取消时执行异步清理,可以在
onCancel中启动一个新的Task,但要小心生命周期。
更现代的写法(Swift 5.7+)使用 withTaskCancellationHandler(operation:onCancel:),本质相同。
3.5 任务局部值(TaskLocal)
TaskLocal 是 Swift 并发中的任务本地存储 机制,它允许你在一个任务树中传递一个值,使得该任务及其所有子任务(以及结构化并发下的后续代码)都能读取到这个值,而无需显式通过函数参数传递。
基本继承规则
- 同一任务内 :在
withValue闭包内的所有代码(包括同步和异步部分)都能看到绑定的值。 - 结构化子任务 :
async let、TaskGroup中添加的任务,会自动继承父任务当前的 TaskLocal 值。 - 非结构化
Task:如果使用Task { }创建新任务,会继承 当前任务的 TaskLocal 值(因为Task初始化器捕获了当前任务上下文)。 Task.detached:不会继承任何 TaskLocal 值,所有 TaskLocal 变量都恢复为初始值(除非在 detached 内部重新绑定)。
Swift
enum MyContext {
/// 必须是 static(全局唯一标识符),不能是实例属性。
/// 必须提供初始值(通常为默认值,如空字符串)。
@TaskLocal static var traceIDDD: String = ""
}
// 任务局部值
func testTaskLocal() async {
log("修改前")
// 1. 在当前上下文修改值
// $traceIDDD是访问包装器TaskLocal本身
await MyContext.$traceIDDD.withValue("abc-123") {
// 在此闭包内以及任何从这个闭包启动的任务中,
// 读取 MyContext.traceID 都会得到 "abc-123"
log("修改后直接读取") // "abc-123"
await step1() // 内部可读取到 "abc-123"
await step2()
Task {
log("修改后在Task中读取") // "abc-123"
}
Task.detached { @MainActor in
log("修改后在Task.detached中读取") // "abc-123"
}
testTaskLocal2()// 这方法里面会继承修改后的值,
await withTaskGroup(of: Void.self) { group in
group.addTask {@MainActor in
log("修改后在TaskGroup中读取") // 输出 "parent"(继承)
}
}
}
// 退出闭包后,恢复原来的值(通常为空字符串)
}
- 为什么要设计这个角色
TaskLocal?
解决的问题:避免参数传递污染
在没有
TaskLocal时,如果需要在整个调用链中传递一个上下文值(如 trace ID),你不得不把它作为参数显式传递给每一个函数:
TaskLocal原理 基于 任务私有存储字典 实现。- 每个
Task对象内部持有一个[ObjectIdentifier: Any]字典,用于存储所有 TaskLocal 变量的值。 - 每个
TaskLocal变量(静态属性)有一个唯一的ObjectIdentifier作为键。
3.6 本质
| 维度 | 说明 |
|---|---|
| 轻量级 | Task 不是线程,是协程调度单元;创建开销极小(纳秒级),由 Swift 运行时调度到线程池执行 |
| 结构化 vs 非结构化 | Task {} 创建的是非结构化任务 (独立生命周期);async let 和 TaskGroup 属于结构化并发(父子生命周期绑定) |
| 协作式取消 | 取消是请求式 的,不是强制的;需要在任务内部主动检查 Task.isCancelled 或使用 try Task.checkCancellation() |
| 优先级传播 | 子任务继承父任务优先级;优先级可动态提升(priority escalation) |
| 与 GCD 对比 | Task 替代 DispatchQueue.async;但 Task 有取消、优先级、结构化生命周期,GCD 没有 |
四、TaskGroup-结构化任务组
TaskGroup 是一个任务容器 ,你可以向其中添加多个子任务(addTask),然后等待所有子任务完成(waitForAll)或逐个收集结果(next())。它与 async let 的主要区别在于:子任务数量在运行时动态确定 (例如从数组循环添加),而 async let 需要编译时固定数量。
两种形式:
withTaskGroup:子任务不抛出错误,或你不关心错误。withThrowingTaskGroup:子任务可以抛出错误,且错误会自动传播到组的作用域。
设计目标是将并发任务的生命周期纳入编程语言的语法作用域管理,从而解决传统并发模型中常见的"任务泄漏"、取消困难、错误传播混乱等问题。
4.1 基本用法
swift
func testTaskGroup(urls: [URL]) async {
let withTaskGroupresult = await withTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask {
await download(from: url) ?? Data()
}
}
var results: [Data] = []
// for await 是 Swift 并发中用于迭代 异步序列(AsyncSequence) 的语法。
for await data in group {
results.append(data)
}
return results // results.map({ String(data: $0, encoding: .utf8)})
}
print(withTaskGroupresult)// 这里就是里面最后返回的results
}
| 特性 | 说明 |
|---|---|
| 结构化生命周期 | 任务组的生命周期绑定到 withTaskGroup 闭包。闭包返回前,编译器会隐式等待所有子任务完成(或取消)。不可能"泄漏"子任务。 |
| 错误传播 | 在 withThrowingTaskGroup 中,任意子任务抛出错误会立即取消整个组,并从 next() 或 waitForAll() 抛出。 |
| 取消传播 | 如果任务组本身被取消(例如父任务取消),组内所有未完成的子任务会自动收到取消信号。 |
| 动态并发 | 子任务数量不限,可以在循环中动态添加,非常适合处理集合。 |
| 结果无序处理 | 通过 for await 或 next() 按完成顺序处理结果,无需等待所有任务完成。 |
4.2 异步序列语法-AsyncSequence
for await 是 Swift 并发中用于迭代 异步序列(AsyncSequence) 的语法。TaskGroup 本身遵循 AsyncSequence 协议,因此你可以直接使用 for await 来遍历组中已完成子任务的结果 ,按完成顺序依次获取。
for await 底层实际上是反复调用 group.next() 方法,直到它返回 nil:
swift
while let data = await group.next() {
process(data)
}
-
next()返回Optional<ChildTaskResult>:- 当有子任务完成时,返回其结果(
.some(result))。 - 当所有子任务都已完成(且没有更多结果时),返回
nil表示迭代结束。
- 当有子任务完成时,返回其结果(
-
对于
withThrowingTaskGroup,next()会抛出错误(如果子任务抛出),因此需要try await group.next()。
4.3 控制最大并发数
Swift
func testMaxConcurrent() async {
await withTaskGroup(of: Data.self) { group in
let maxConcurrent = 3
var iterator = urls.makeIterator()
// 先启动 maxConcurrent 个子任务
for _ in 0..<maxConcurrent {
guard let url = iterator.next() else { break }
group.addTask { await download(from: url) ?? Data() }
}
var results: [Data] = []
// 每当一个子任务完成,就启动下一个
while let aResult = await group.next() {
results.append(aResult)
print("完成:results.count=\(results.count)")
if let url = iterator.next() {
print("追加一个")
group.addTask { await download(from: url) ?? Data() }
}
}
}
}
4.4 withDiscardingTaskGroup
withDiscardingTaskGroup 是 iOS 17 / macOS 14 引入的结构化并发原语,它与普通 TaskGroup 最大的区别在于:子任务一旦完成,其结果和资源会被立即丢弃,不会堆积在内存中等待 next() 消费。
举例:网络服务器监听连接循环
这是 Apple 在提案中给出的最经典示例。一个 TCP 服务器需要不断接受新连接并为每个连接创建处理任务。如果用普通 withThrowingTaskGroup,子任务结果会一直累积,直到你调用 next()------但服务器永远在 while 循环中等待新连接,根本没机会去消费,最终导致内存泄漏。
csharp
swift
try await withThrowingDiscardingTaskGroup() { group in
while let newConnection = try await listeningSocket.accept() {
group.addTask {
handleConnection(newConnection)
}
// 每个连接处理完成后自动释放,无需手动 next()
}
}
这里每次 handleConnection 执行完毕,对应的子任务资源和结果就会被立即回收,无论循环运行多久,内存都是稳定的。
4.5 本质
| 维度 | 说明 |
|---|---|
| 结构化并发核心 | 父任务等待所有子任务完成;子任务的生命周期不超出父任务的作用域 |
| 错误传播 | 任何子任务抛出错误 → 其他子任务自动取消 → 错误传播到父任务 |
| 取消传播 | 父任务取消 → 所有子任务级联取消 |
| 背压(Backpressure) | for await 消费速度控制生产速度;addTask 不阻塞,但 AsyncIterator 消费是串行的 |
| 同构 vs 异构 | TaskGroup 是同构 的(所有子任务返回相同类型);异构并发用 async let |
五、Sendable-可跨并发域传递的类型协议
Sendable 协议就是一个编译时标记 ,告诉编译器:"我这个类型可以安全地跨并发边界传递"。编译器会检查符合 Sendable 的类型的内部实现,确保它们确实是线程安全的。
5.1 编译器推断Sendable方式
Swift
// ✅ 自动 Sendable:所有属性是 Sendable 的值类型
struct User: Sendable {
let id: Int // Int 是 Sendable
var name: String // String 是 Sendable
// 注意:虽然 name 是 var,但整个 struct 仍然是 Sendable?编译器会检查。
// 实际上 Swift 5.7+ 中,值类型的所有存储属性都必须是 Sendable 才能自动符合。
// 但值类型本身不可变?不,值类型传递时是拷贝,所以即使有 var 属性也是安全的,
// 因为不同任务持有不同的拷贝。所以值类型会自动 Sendable 只要成员 Sendable。
}
如果类型满足以下条件之一,编译器会自动推断它符合 Sendable:
- 值类型 (
struct、enum)且所有成员都是Sendable。 actor类型自动符合Sendable(因为它自身提供隔离)。- 不可变的类 (即所有存储属性是
let且类型也是Sendable)。 - 某些标准库类型被显式标记为
@unchecked Sendable(如Int、String、Array<Sendable>?等)。
一般警告解决方法:
- 改为
actor。 - 或使用锁、串行队列等保护,然后标记为
@unchecked Sendable(告诉编译器"我保证安全")。 - 闭包作为参数时标记@sendable
5.2 @Sendable与sending
- 有的系统方法里面参数使用sending标记,有何区别?
swift
// `sending` @isolated(any)是新的**参数修饰符**(Swift 6 引入)
func adasss() async {
var ad = MyThreadSafeClass()// 是一个普通类
let account = BankAccount() // 是一个actor
// 如果下面的operation参数没有标记Sending这里会报错:
// Sending value of non-Sendable type '@concurrent () async -> ()' risks causing data races
await execute(on: account) {
print("ad=\(ad.cityId2)")
}
}
// 参数 actor 的类型是 isolated(any) Actor
// operation需要加@sendable还是sending根据情况写
func execute(on actor: isolated(any Actor), operation: () async -> Void) async {
await operation()
}
/* 报错的原因:
1. **闭包的类型和 `Sendable` 要求**
在 Swift 并发模型中,**闭包默认不是 `Sendable`**,除非显式标记为 `@Sendable`。
非 `Sendable` 的闭包如果被跨并发域传递,可能导致数据竞争(例如捕获了可变状态)。
因此,编译器禁止将非 `Sendable` 闭包传递给可能运行在另一个执行器上的上下文。
2. **`sending` 的作用**
`sending` 参数修饰符告诉编译器:这个闭包将被"发送"到其他并发域,调用方在传入后**放弃对闭包内容(及其捕获的值)的进一步访问**。
这允许编译器放宽对闭包本身是 `Sendable` 的要求,因为所有权已经转移,不会出现同时访问。
你之前加了 `sending`,编译器认可这种所有权转移,因此不要求闭包是 `Sendable`。
去掉 `sending` 后,编译器认为这是一个普通的非 `Sendable` 闭包,却要跨 actor 调用,自然报错。
*/
sending 的优势:让安全的代码写起来更灵活
sending 的最大价值是解决了传统并发模型中"为了传递一个值,必须让它变成 Sendable "的僵化问题。例如,一个临时的、仅在单线程内使用的可变对象,在需要被安全转移时,就可以使用 sending 来明确这种瞬时的所有权转移,而无需大费周章地修改它的整个类型定义。
-
@Sendable是一种静态的类型层面的承诺,它告诉我们:这个"公民"(类型)本质上是安全的。 -
sending是一种动态的值传递层面的规则,它确保:这个"快递"(值)在传递过程中是安全的,哪怕它不是"安全公民"。 -
@isolated(any)标记是何意?
@isolated(any) 是 Swift 6 中引入的一个参数修饰符 ,用于在函数签名中声明一个"无具体类型的执行器隔离 "参数。它允许你编写可以接受任意 actor 类型的函数,而无需在泛型中显式指定具体的 actor 类型。
js
// 新写法:参数 actor 的类型是 isolated(any Actor) ,可以是任意的一个类型
func execute(on actor: isolated(any Actor), operation: () async -> Void) async {
await operation()
}
// 旧写法:需要写明BankAccount这个actor类型
func transfer(from: isolated BankAccount, to: BankAccount, amount: Double) async {
from.withdraw(amount) // 无需 await
await to.deposit(amount)
}
5.2 本质
| 维度 | 说明 |
|---|---|
| 编译期检查协议 | Sendable 是一个 marker protocol (标记协议),无任何方法要求,仅作为编译器约束标记 |
| 值类型 vs 引用类型 | 值类型(struct/enum)拷贝语义天然隔离 → 通常安全;引用类型共享内存 → 通常不安全 |
| 传递语义 | Sendable 保证的是跨并发域传递时安全 ,不是"永远线程安全";它约束的是传递瞬间的数据快照 |
| Strict Concurrency | Swift 6 默认严格并发检查;Swift 5.x 需要手动开启 StrictConcurrency |
六. AsyncSequence/AsyncStream---异步数据流
6.1 AsyncSequence 协议
- 作用 :异步产生值的序列,类似同步的
Sequence,但每次获取下一个元素可能需要等待(await)。
swift
for try await line in url.lines {
print(line)
}
6.2 AsyncStream
- 作用 :构建自定义的
AsyncSequence的最简单方式,用于生产‑消费模式,可手动发送值并结束。
Swift
// 2) AsyncStream --- 从回调构建异步流
let stream = AsyncStream<Int> { continuation in
socket.onMessage { data in
continuation.yield(data) // 产生值
}
socket.onClose {
continuation.finish() // 结束流
}
}
for await value in stream {
print(value)
}
七、Continuation --- 回调桥接器
withCheckedContinuation:略慢,在调试模式下检查:是否恢复一次,是否重复恢复withUnsafeContinuation:最快,无检查,行为未定义
这两个函数是 Swift 并发中用于将基于回调的异步 API 转换为 async/await 模式 的关键工具。它们允许你手动创建一个 Continuation 对象,该对象可以在回调触发时恢复暂停的异步任务。
swift
// 为 iOS 13+ 提供兼容方案
func openURL(_ url: URL) async -> Bool {
if #available(iOS 13.0, *) {
return await UIApplication.shared.open(url)
} else {
// 使用 continuation 桥接到 async/await
return await withCheckedContinuation { continuation in
UIApplication.shared.open(url) { isSuc in
continuation.resume(returning: isSuc)
}
}
}
}
// 使用 UnsafeContinuation 时,你必须手动保证 resume 被恰好调用一次,否则会导致内存泄漏、任务永挂起或崩溃。
func openURL2(_ url: URL) async -> Bool {
if #available(iOS 13.0, *) {
return await UIApplication.shared.open(url)
} else {
// 使用 continuation 桥接到 async/await
return await withUnsafeContinuation { continuation in
UIApplication.shared.open(url) { isSuc in
continuation.resume(returning: isSuc)
}
}
}
}
八、各个角色实现原理
1. async/await原理
swift
┌─────────────────────────────────────────────────────────────────┐
│ <<protocol>> │
│ Executor │
├─────────────────────────────────────────────────────────────────┤
│ + enqueue(_ job: UnownedJob) │
│ (将可执行单元加入调度队列) │
└─────────────────────────────────────────────────────────────────┘
▲
│ 实现
┌───────────────────┼───────────────────┐
│ │ │
┌─────────┴─────────┐ ┌───────┴────────┐ ┌─────────┴─────────┐
│ SerialExecutor │ │ ConcurrentEx.. │ │ MainActor │
│ (串行执行器) │ │ (全局并发执行器) │ │ (专用串行执行器) │
└───────────────────┘ └────────────────┘ └───────────────────┘
│ │
└─────────┬──────────┘
│
┌─────────────▼─────────────┐
│ ExecutorJob │
├───────────────────────────┤
│ + run() │
│ + runSynchronously(on:) │
└─────────────┬─────────────┘
│ 创建
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
┌─────────▼─────────┐ ┌───────────▼──────────┐ ┌─────────▼─────────┐
│ Continuation │ │ AsyncFunctionFrame │ │ AwaitPoint │
│ (续体) │ │ (异步函数栈帧) │ │ (挂起点描述) │
├───────────────────┤ ├──────────────────────┤ ├───────────────────┤
│ - resume() │ │ - localVariables │ │ - resumeAddress │
│ - resume(throwing:)│ │ - awaitPoints[] │ │ - capturedContext │
│ - 保存上下文 │ │ - currentPC │ └───────────────────┘
└─────────┬─────────┘ └──────────┬───────────┘
│ │
└────────────┬───────────────┘
│
┌─────────────▼─────────────┐
│ Swift Runtime │
│ (异步函数状态机转换) │
└───────────────────────────┘
各角色职责与协作流程
| 角色 | 职责 | 关键协作 |
|---|---|---|
| Executor | 调度执行单元(Job)。不同执行器提供不同调度策略(串行/并发/主线程)。 | enqueue 接收 UnownedJob,将其放入队列并在适当时机调用 job.run()。 |
| ExecutorJob | 可执行的单元,封装一个异步函数的执行片段(从某个挂起点到下一个挂起点或结束)。 | 由编译器为每个 async 函数生成。run() 执行当前片段,遇到 await 时返回并保存状态。 |
| Continuation | 代表一个挂起点的"恢复令牌"。外部(如回调)通过 resume() 让挂起的任务继续执行。 |
由 withUnsafeContinuation 创建,传递给异步操作。操作完成后调用 resume,将 ExecutorJob 重新入队。 |
| AsyncFunctionFrame | 异步函数的栈帧,存储局部变量和挂起点列表。在堆上分配,生命周期跨越多次挂起/恢复。 | 编译器将其布局为结构体,每个 await 对应一个状态机中的状态。 |
| AwaitPoint | 记录一个 await 的位置,包括恢复地址、需要保存的局部变量等。 |
由编译器生成,嵌入在 AsyncFunctionFrame 中。 |
| Swift Runtime | 提供底层基础:任务创建、执行器获取、取消传播、错误转发等。 | 协调所有组件,执行状态机转换。 |
javascript
状态机视角下AsyncFunctionFrame角色该有的内容:
┌────────────────────────────────────────────────────────────────────┐
│ AsyncFunction │
│ (编译器为每个async函数生成) │
├────────────────────────────────────────────────────────────────────┤
│ + 状态枚举: State { case start, afterAwait1, afterAwait2, done } │
│ + 当前状态: currentState │
│ + 局部变量: locals (堆分配) │
│ + 执行(continuation: Continuation) │
└────────────────────────────────┬───────────────────────────────────┘
**`AsyncFunction`** 和 **`AsyncFunctionFrame`** 是同一运行时对象的两个名字,
分别强调其**行为**(状态机)和**存储**(帧数据)。
协作流程(以调用异步函数为例)
-
调用开始
调用一个
async函数时,编译器在堆上分配AsyncFunctionFrame,存储初始局部变量。当前执行器(例如全局并发执行器)创建一个ExecutorJob指向该帧的起始点,并调用executor.enqueue(job)。 -
执行到第一个
await执行器调用
job.run(),运行帧中当前状态对应的代码段。当遇到await expression时:- 编译器生成代码:捕获需要保留的局部变量到
AsyncFunctionFrame。 - 创建一个
Continuation对象,封装恢复所需的所有信息(包括帧指针、下个状态编号)。 - 将
Continuation传递给被await的异步函数(或桥接回调)。 - 当前
ExecutorJob的run()返回,任务挂起。
- 编译器生成代码:捕获需要保留的局部变量到
-
等待操作完成
被
await的异步函数最终通过continuation.resume()(或.resume(throwing:))通知完成。resume内部:- 将关联的
ExecutorJob重新入队到原来的执行器(或根据executorPreference指定的执行器)。 - 执行器稍后再次调用
job.run(),这次从上一个挂起点之后的地址继续执行。
- 将关联的
-
恢复执行
job.run()被再次调用时,它从AsyncFunctionFrame中读取恢复地址和保存的局部变量,继续执行后续代码,直到遇到下一个await或函数返回。 -
函数返回
当执行到
return语句时,AsyncFunctionFrame被释放(或回收),ExecutorJob标记为完成,并向任何等待该结果的调用者发送返回值。
2. Actor内部原理
js
┌─────────────────────────────────────────────────────────────────────┐
│ Actor │
│ (每个 Actor 实例) │
├─────────────────────────────────────────────────────────────────────┤
│ - state: ActorState // 隔离的可变状态(存储属性) │
│ - executor: SerialExecutor // 串行执行器(每个 Actor 一个) │
│ - jobQueue: Queue<Job> // 待执行的任务队列(通常由 executor 管理)│
│ - isSuspended: Bool // 是否正挂起等待某个异步操作完成 │
├─────────────────────────────────────────────────────────────────────┤
│ + enqueue(job: Job) // 外部调用入口(编译器生成) │
│ + runJob(_ job: Job) // 执行队列中的下一个任务 │
└─────────────────────────────────────────────────────────────────────┘
│
│ 使用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ <<protocol>> │
│ SerialExecutor │
├─────────────────────────────────────────────────────────────────────┤
│ + enqueue(_ job: UnownedJob) │
│ + isSame(as other: SerialExecutor) -> Bool │
└───────────────────────────┬─────────────────────────────────────────┘
│ 实现
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ActorSpecificExecutor │
│ (编译器为每个 Actor 自动生成) │
├─────────────────────────────────────────────────────────────────────┤
│ - actor: UnownedActorRef // 指向所属 Actor 实例 │
│ - queue: DispatchQueue? // 可基于 GCD 或自定义线程池 │
├─────────────────────────────────────────────────────────────────────┤
│ + enqueue(_ job: UnownedJob) │
│ └─ 将 job 放入内部队列,若当前无活跃任务则立即调度执行 │
└─────────────────────────────────────────────────────────────────────┘
│
│ 执行
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Job │
│ (代表一次对 Actor 的方法调用) │
├─────────────────────────────────────────────────────────────────────┤
│ - function: AsyncFunctionPointer // 要执行的方法 │
│ - arguments: [Any] // 参数 │
│ - continuation: Continuation? // 调用方提供的恢复令牌 │
│ - next: Job? // 指向队列中的下一个任务 │
├─────────────────────────────────────────────────────────────────────┤
│ + run() │
└─────────────────────────────────────────────────────────────────────┘
│
│ 可能包含
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Continuation │
│ (记录调用方挂起点) │
├─────────────────────────────────────────────────────────────────────┤
│ + resume() / resume(throwing:) │
│ + resume(returning:) │
└─────────────────────────────────────────────────────────────────────┘
3. Task内部原理
Swift
┌─────────────────────────────────────────────────────────────────────────┐
│ Task │
│ (用户可见的异步工作单元) │
├─────────────────────────────────────────────────────────────────────────┤
│ - id: UInt64 // 唯一标识 │
│ - priority: TaskPriority // 优先级(继承或指定) │
│ - isCancelled: Bool // 取消标志 │
│ - state: TaskState // 状态机(枚举) │
│ - parent: Task? // 父任务(结构化时存在) │
│ - children: Set<Task> // 子任务列表(结构化时) │
│ - taskLocalValues: [ObjectIdentifier: Any] // TaskLocal 存储字典 │
│ - executor: Executor // 当前关联的执行器 │
│ - job: UnownedJob // 可执行的底层作业 │
│ - result: TaskResult<Success, Failure>? // 最终结果或错误 │
├─────────────────────────────────────────────────────────────────────────┤
│ + cancel() │
│ + getValue() async throws -> Success │
│ + run() (内部) │
└─────────────────────────────────────────────────────────────────────────┘
│
│ 使用
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ <<protocol>> │
│ Executor │
├─────────────────────────────────────────────────────────────────────────┤
│ + enqueue(_ job: UnownedJob) │
│ + asUnownedSerialExecutor() -> UnownedSerialExecutor? (可选) │
└───────────────────────────┬─────────────────────────────────────────────┘
│ 实现
┌─────────────────┼─────────────────┬─────────────────────────┐
▼ ▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GlobalConcurrent│ │ MainActor │ │ SerialExecutor │ │ CustomExecutor │
│ Executor │ │ (Executor) │ │ (包装串行队列) │ │ (用户自定义) │
│ (单例) │ │ (单例) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
▲
│ 使用
┌─────────────────────────────┴───────────────────────────────────────────┐
│ UnownedJob │
├─────────────────────────────────────────────────────────────────────────┤
│ - functionPointer: UnsafeRawPointer // 指向任务闭包的入口点 │
│ - context: JobContext // 捕获的上下文(局部变量等) │
├─────────────────────────────────────────────────────────────────────────┤
│ + run() │
│ + runSynchronously(on executor: UnownedSerialExecutor) │
└─────────────────────────────────────────────────────────────────────────┘
│
│ 内部包含/创建
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Continuation │
├─────────────────────────────────────────────────────────────────────────┤
│ - task: UnownedTaskRef // 所属任务 │
│ - state: ContinuationState // 挂起时的状态(恢复点) │
│ - capturedLocals: [Any] // 挂起时保存的局部变量 │
│ - resumeAddress: UnsafeRawPointer // 恢复后的执行地址 │
├─────────────────────────────────────────────────────────────────────────┤
│ + resume() │
│ + resume(returning value: Success) │
│ + resume(throwing error: Failure) │
└─────────────────────────────────────────────────────────────────────────┘
4. TaskGroup内部原理
以下类图聚焦于 TaskGroup 的内部组件与协作关系,展示其如何管理子任务、收集结果、处理取消和错误,并实现结构化并发。
plaintext
┌─────────────────────────────────────────────────────────────────────────┐
│ withTaskGroup / withThrowingTaskGroup │
│ (入口函数) │
├─────────────────────────────────────────────────────────────────────────┤
│ + 创建 TaskGroup 实例 │
│ + 调用用户闭包 │
│ + 离开闭包时隐式调用 waitForAll() │
└─────────────────────────────────┬───────────────────────────────────────┘
│ 创建
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ TaskGroup<ChildTaskResult> │
│ (用户通过闭包参数 group 访问) │
├─────────────────────────────────────────────────────────────────────────┤
│ - storage: GroupStorage<ChildTaskResult> // 共享状态存储 │
│ - isCancelled: Bool // 组级别取消标志 │
│ - parentTask: Task? // 所属的父任务(结构化) │
├─────────────────────────────────────────────────────────────────────────┤
│ + addTask(operation: () async -> ChildTaskResult) │
│ + addTaskUnlessCancelled(...) // 条件添加 │
│ + cancelAll() │
│ + waitForAll() async // 等待所有子任务完成 │
│ + next() async -> ChildTaskResult? // 获取下一个完成的结果 │
│ + isEmpty: Bool // 是否有未完成子任务 │
└─────────────────────────────────┬───────────────────────────────────────┘
│ 使用
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ GroupStorage<ChildTaskResult> │
│ (堆上分配,所有子任务共享) │
├─────────────────────────────────────────────────────────────────────────┤
│ - childTasks: Set<ChildTaskRef> // 活跃子任务列表 │
│ - resultQueue: AsyncStream<ChildTaskResult>.Continuation? // 结果队列 │
│ - nextContinuations: [Continuation] // 等待 next() 的挂起任务 │
│ - isComplete: Bool // 所有子任务已完成标志 │
│ - cancelFlag: Bool // 组取消标志的原子存储 │
│ - lock: Lock // 内部同步原语 │
├─────────────────────────────────────────────────────────────────────────┤
│ + add(child: ChildTaskRef) │
│ + remove(child: ChildTaskRef) │
│ + enqueue(result: ChildTaskResult) // 子任务完成时调用 │
│ + tryGetNextResult() -> ChildTaskResult? // 非阻塞获取结果 │
│ + registerNextContinuation(_ continuation: Continuation) │
│ + cancelAllChildren() // 遍历 childTasks 逐个取消 │
└─────────────────────────────────┬───────────────────────────────────────┘
│ 管理
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ ChildTaskRef │
│ (对底层子任务的引用,包装 Task 对象) │
├─────────────────────────────────────────────────────────────────────────┤
│ - task: Task<ChildTaskResult, Never> // 实际执行任务的 Task │
│ - id: UInt64 │
│ - groupStorage: WeakRef<GroupStorage> // 弱引用所属组 │
├─────────────────────────────────────────────────────────────────────────┤
│ + cancel() │
│ + isCompleted: Bool │
└─────────────────────────────────────────────────────────────────────────┘
- 各角色职责与配合工作
| 角色 | 职责 | 关键配合 |
|---|---|---|
withTaskGroup 入口函数 |
创建 TaskGroup 实例,将用户闭包作为参数传入;在闭包返回后,隐式调用 group.waitForAll(),确保所有子任务完成。 |
负责结构化生命周期的边界。 |
TaskGroup |
用户可见的 API 外观(Facade)。对外提供 addTask、next()、cancelAll() 等方法,内部将操作委托给 GroupStorage。 |
将子任务的添加、结果获取、等待等请求转发给底层存储。 |
GroupStorage |
核心状态存储,线程安全(通过内部锁)。维护活跃子任务集合、结果队列、等待 next() 的延续列表。 |
子任务完成时调用 enqueue(result:);next() 调用时尝试从队列取结果,无结果则挂起并保存延续。 |
ChildTaskRef |
对每个子任务(Task 对象)的包装,持有对 GroupStorage 的弱引用,以便完成时通知组。 |
子任务执行完毕后,通过弱引用调用 groupStorage.enqueue(result:),然后从子任务集合中移除自身。 |
| 内部锁(Lock) | 保护 GroupStorage 内部数据结构(childTasks、resultQueue 等)的并发访问,因为多个子任务可能同时完成,而主任务也可能同时调用 next()。 |
使用轻量级自旋锁或 os_unfair_lock。 |
- 配合工作流程(以添加两个子任务并迭代结果为例)
-
初始化
withTaskGroup创建TaskGroup实例,后者创建GroupStorage(堆上)。 -
添加子任务
- 用户调用
group.addTask { ... }。 TaskGroup调用GroupStorage.add(child:)将子任务加入集合。- 创建
Task执行闭包,同时给ChildTaskRef设置一个完成回调:当Task完成时,回调GroupStorage.enqueue(result:)。
- 用户调用
-
子任务完成
- 假设子任务 A 先完成。其完成回调调用
groupStorage.enqueue(resultA)。 enqueue使用锁保护:将resultA放入结果队列。- 检查是否有正在等待
next()的延续(存储在nextContinuations中)。若有,取出第一个延续,恢复它并将结果直接传递。
- 假设子任务 A 先完成。其完成回调调用
-
用户迭代结果
- 用户代码执行
for await result in group。 - 底层调用
group.next():
a. 加锁,尝试从结果队列取一个结果。若有,立即返回。
b. 若无结果,检查是否还有未完成子任务(childTasks非空)。
c. 若还有未完成子任务,则挂起当前任务,将其延续保存到nextContinuations。
d. 若没有未完成子任务,返回nil结束迭代。
- 用户代码执行
-
所有子任务完成
- 最后一个子任务完成并调用
enqueue后,GroupStorage检测到childTasks变为空。 - 如果有任何
nextContinuations仍在等待,向它们发送nil(结束信号)。 - 设置
isComplete = true。
- 最后一个子任务完成并调用
-
离开作用域
withTaskGroup闭包返回后,编译器插入隐式await group.waitForAll()(实际上就是反复调用next()直到nil)。这保证了所有子任务确实已完成。
九、结构化并发 vs 非结构化并发
9.1 结构化并发(Structured Concurrency)
特征:任务生命周期与作用域绑定,离开作用域前自动等待所有子任务完成,错误/取消自动传播。
| 构造 | 说明 |
|---|---|
async let |
绑定到当前作用域,离开前自动 await |
TaskGroup (withTaskGroup / withThrowingTaskGroup) |
组内子任务结构化,组结束前自动等待 |
async 函数调用链 |
通过 await 形成结构化层级 |
await 表达式 |
在当前任务中等待子任务 |
9.2 非结构化并发(Unstructured Concurrency)
特征:任务独立于创建作用域,可能"逃逸",生命周期不受父任务约束。
| 构造 | 说明 |
|---|---|
Task { } |
不绑定作用域,创建后独立运行 |
Task.detached { } |
完全不继承父任务上下文(优先级、actor等) |
存储 Task 对象 |
即使后续 await,创建时已逃逸 |
Task+ 手动await task.value:虽能等待结果,但离开作用域不会自动取消/等待,仍属非结构化。@MainActor标记的Task:依然是非结构化,只是绑定到主执行器。
结语
Swift 的现代并发模型代表了并发编程的范式转变:
- 从手动调度到智能调度 - 信任运行时做出最优决策
- 从回调地狱到线性代码 - 使用 async/await 简化异步流程
- 从容易出错到内存安全 - 通过 Actor 和值语义避免数据竞争
- 从复杂管理到结构化 - 自动处理任务生命周期和取消
虽然学习曲线比 GCD 更陡峭,但一旦掌握,你将能编写出更安全、更简洁、更高效的并发代码。
进一步学习资源:
