Swift async/await并发框架深入总结

7 阅读38分钟

引言:为什么需要新的并发模型?

在传统 iOS/macOS 开发中,我们使用 GCD(Grand Central Dispatch)或 OperationQueue 来处理并发任务。然而,这些技术存在一些痛点:

  1. 回调地狱:多层嵌套的回调难以阅读和维护
  2. 手动内存管理:容易忘记 weak self 导致内存泄漏
  3. 线程爆炸:过度创建线程消耗系统资源
  4. 数据竞争:共享状态需要手动加锁,容易出错

Swift 5.5 引入的 async/await 和结构化并发解决了这些问题,提供了更安全、更简洁的并发编程方式,iOS 13以上是支持的。

AI很多是错误的,因此下面是经过自己Xcode 26.5手动尝试的总结,作为一个手册方便后续查看。

整体架构

类结构图,先对整体角色职责有个了解:

角色架构图.png

一、async/await 基础语法

1.1 异步函数声明

// 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 异步函数调用

// 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()
}

异步属性

// 异步属性
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并发启动多个任务

// 同时启动多个异步任务
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 子任务‌。根据结构化并发的规则:

  1. 隐式取消‌:运行时会‌自动向 child1 和 child2 发送取消信号‌。
  2. 隐式等待‌:父任务‌不会立即返回‌,而是隐式地 await 这两个子任务直到它们响应取消并结束。
  3. 抛出错误‌:如果 longWork() 和 anotherWork() 内部有检查取消状态(如 try Task.checkCancellation()),它们会抛出 ‌CancellationError,这个错误会被该作用域外层的 catch 捕获。
// 后面没有await的话,子任务会被取消
Task { 
    async let child1 = longWork() // 启动子任务 1 
    async let child2 = anotherWork() // 启动子任务 2 
    // ⚠️ 直接退出作用域,没有 await child1 和 child2 
}

1.4 async/await执行对比

总耗时:

// 并发执行(总耗时 ≈ 最慢的任务)
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秒

异常:

// 串行:遇到第一个错误立即停止
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 使用注意点

  1. await 不是线程切换await 只表示"可能挂起",恢复时不保证回到同一线程
  2. 不要在 deinit 中使用 asyncdeinit 是同步的,对象可能已释放
  3. async let 的生命周期:必须在作用域结束前 await,否则任务取消但不会报错
  4. 重入问题actor 的 async 方法在 await 挂起后恢复时,隔离状态可能已变,这是 actor 重入的根源
  5. 死锁风险:同步函数中无法调用 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) 的上下文中。
// 情况一:
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基本使用

// 一、修饰类型定义,相当于类
// 内部访问无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
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隔离可以修饰多个类型。

// 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

// 在任何线程
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任务挂起点让出执行权,允许其他任务进入执行,这就是重入。

  1. 当前任务被挂起(suspend),并暂时释放 Actor 的执行器
  2. 执行器立即从队列中取出下一个任务开始执行。
  3. 当被挂起的任务恢复时,它会重新进入队列末尾(或按优先级插队),等待再次获得执行权。
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 的另一个非异步方法

// 
actor MyActor {
    func methodA() {
        methodB()   // 直接同步调用,无需 await
    }
    
    func methodB() {
        print("ok")
    }
}
  • methodA 和 methodB 运行在同一个任务中,当前任务已经持有了该 actor 的执行权。
  • 调用 methodB 只是当前任务内部的函数调用,不会排队、不会挂起,所以没有机会形成死锁

场景2:调用同一 actor 的另一个异步方法

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 的方法

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())都会进入这个队列。
  • 执行器一次只允许一个任务通过隧道(执行任务代码)。
  • 其他任务在隧道外排队等候,直到当前任务完全结束主动让出
// 示意图:Actor 的串行队列
┌───────────────────────────┐
 Actor 的执行器队列         
 ┌─────┬─────┬─────┐       
  T1   T2   T3   ...   
 └──╥──┴─────┴─────┘       
      一次只执行一个        
                          
 ┌─────────────┐           
  正在执行 T1             
 └─────────────┘           
└───────────────────────────┘

实现机制:Swift 运行时为每个 Actor 分配一个 UnownedSerialExecutor(可自定义)。提交任务时,任务被包装成 Job 并入队;执行器出队并执行 job.run()

三、Task-异步任务单元

Task 是 Swift 并发中的基本执行单元,代表一个异步操作的实例。它提供了创建、管理、取消异步操作的接口。

3.1. Task基本使用

  • 创建即提交 - 不需要额外的 API
  • 当前线程不阻塞 - 调度到其他线程执行
  • 优先级决定顺序 - 但都晚于当前同步代码
  • 无法"挂起"启动 - 如果需要延迟,在{}内部自己await等待
// 一、简单调用:不需要获取返回值
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 稳定的同时,为未来扩展留下空间.

// 优先级等级
// .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)”上。  这是一个由整个运行时共享的、用于执行没有严格执行器要求的任何工作的线程池

// 任务分离
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的取消相关。
// 直接 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 继续执行
}
  • 取消状态的判断
// 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
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 letTaskGroup 中添加的任务,会自动继承父任务当前的 TaskLocal 值
  • 非结构化 Task:如果使用 Task { } 创建新任务,会继承当前任务的 TaskLocal 值(因为 Task 初始化器捕获了当前任务上下文)。
  • Task.detached不会继承任何 TaskLocal 值,所有 TaskLocal 变量都恢复为初始值(除非在 detached 内部重新绑定)。
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 基本用法

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

while let data = await group.next() {
    process(data)
}
  • next() 返回 Optional<ChildTaskResult>

    • 当有子任务完成时,返回其结果(.some(result))。
    • 当所有子任务都已完成(且没有更多结果时),返回 nil 表示迭代结束。
  • 对于 withThrowingTaskGroupnext() 会抛出错误(如果子任务抛出),因此需要 try await group.next()

4.3 控制最大并发数

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 循环中等待新连接,根本没机会去消费,最终导致‌内存泄漏‌。

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方式

// ✅ 自动 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

  • 值类型structenum)且所有成员都是 Sendable
  • actor 类型自动符合 Sendable(因为它自身提供隔离)。
  • 不可变的类(即所有存储属性是 let 且类型也是 Sendable)。
  • 某些标准库类型被显式标记为 @unchecked Sendable(如 IntStringArray<Sendable>? 等)。

一般警告解决方法:

  • 改为 actor
  • 或使用锁、串行队列等保护,然后标记为 @unchecked Sendable(告诉编译器“我保证安全”)。
  • 闭包作为参数时标记@sendable

5.2 @Sendable与sending

  • 有的系统方法里面参数使用sending标记,有何区别?
// `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 类型。

// 新写法:参数 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 ConcurrencySwift 6 默认严格并发检查;Swift 5.x 需要手动开启 StrictConcurrency

六. AsyncSequence/AsyncStream—异步数据流

6.1 AsyncSequence 协议

  • 作用:异步产生值的序列,类似同步的 Sequence,但每次获取下一个元素可能需要等待(await)。
for try await line in url.lines {
    print(line)
}

6.2 AsyncStream

  • 作用:构建自定义的 AsyncSequence 的最简单方式,用于生产‑消费模式,可手动发送值并结束。
// 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 对象,该对象可以在回调触发时恢复暂停的异步任务。

// 为 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原理

┌─────────────────────────────────────────────────────────────────┐
                          <<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提供底层基础:任务创建、执行器获取、取消传播、错误转发等。协调所有组件,执行状态机转换。
状态机视角下AsyncFunctionFrame角色该有的内容:
┌────────────────────────────────────────────────────────────────────┐
│                         AsyncFunction                              │
│                    (编译器为每个async函数生成)                       │
├────────────────────────────────────────────────────────────────────┤
│ + 状态枚举: State { case start, afterAwait1, afterAwait2, done }   │
│ + 当前状态: currentState                                            │
│ + 局部变量: locals (堆分配)                                          │
│ + 执行(continuation: Continuation)                                 │
└────────────────────────────────┬───────────────────────────────────┘
**`AsyncFunction`** 和 **`AsyncFunctionFrame`** 是同一运行时对象的两个名字,
分别强调其**行为**(状态机)和**存储**(帧数据)。

协作流程(以调用异步函数为例)

  1. 调用开始
    调用一个 async 函数时,编译器在堆上分配 AsyncFunctionFrame,存储初始局部变量。当前执行器(例如全局并发执行器)创建一个 ExecutorJob 指向该帧的起始点,并调用 executor.enqueue(job)

  2. 执行到第一个 await
    执行器调用 job.run(),运行帧中当前状态对应的代码段。当遇到 await expression 时:

    • 编译器生成代码:捕获需要保留的局部变量到 AsyncFunctionFrame
    • 创建一个 Continuation 对象,封装恢复所需的所有信息(包括帧指针、下个状态编号)。
    • 将 Continuation 传递给被 await 的异步函数(或桥接回调)。
    • 当前 ExecutorJob 的 run() 返回,任务挂起。
  3. 等待操作完成
    被 await 的异步函数最终通过 continuation.resume()(或 .resume(throwing:))通知完成。
    resume 内部:

    • 将关联的 ExecutorJob 重新入队到原来的执行器(或根据 executorPreference 指定的执行器)。
    • 执行器稍后再次调用 job.run(),这次从上一个挂起点之后的地址继续执行。
  4. 恢复执行
    job.run() 被再次调用时,它从 AsyncFunctionFrame 中读取恢复地址和保存的局部变量,继续执行后续代码,直到遇到下一个 await 或函数返回。

  5. 函数返回
    当执行到 return 语句时,AsyncFunctionFrame 被释放(或回收),ExecutorJob 标记为完成,并向任何等待该结果的调用者发送返回值。

2. Actor内部原理

┌─────────────────────────────────────────────────────────────────────┐
│                              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内部原理

┌─────────────────────────────────────────────────────────────────────────┐
                                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 的内部组件与协作关系,展示其如何管理子任务、收集结果、处理取消和错误,并实现结构化并发。

┌─────────────────────────────────────────────────────────────────────────┐
│                        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)。对外提供 addTasknext()cancelAll() 等方法,内部将操作委托给 GroupStorage将子任务的添加、结果获取、等待等请求转发给底层存储。
GroupStorage核心状态存储,线程安全(通过内部锁)。维护活跃子任务集合、结果队列、等待 next() 的延续列表。子任务完成时调用 enqueue(result:)next() 调用时尝试从队列取结果,无结果则挂起并保存延续。
ChildTaskRef对每个子任务(Task 对象)的包装,持有对 GroupStorage 的弱引用,以便完成时通知组。子任务执行完毕后,通过弱引用调用 groupStorage.enqueue(result:),然后从子任务集合中移除自身。
内部锁(Lock)保护 GroupStorage 内部数据结构(childTasksresultQueue 等)的并发访问,因为多个子任务可能同时完成,而主任务也可能同时调用 next()使用轻量级自旋锁或 os_unfair_lock
  • 配合工作流程(以添加两个子任务并迭代结果为例)
  1. 初始化
    withTaskGroup 创建 TaskGroup 实例,后者创建 GroupStorage(堆上)。

  2. 添加子任务

    • 用户调用 group.addTask { ... }
    • TaskGroup 调用 GroupStorage.add(child:) 将子任务加入集合。
    • 创建 Task 执行闭包,同时给 ChildTaskRef 设置一个完成回调:当 Task 完成时,回调 GroupStorage.enqueue(result:)
  3. 子任务完成

    • 假设子任务 A 先完成。其完成回调调用 groupStorage.enqueue(resultA)
    • enqueue 使用锁保护:将 resultA 放入结果队列。
    • 检查是否有正在等待 next() 的延续(存储在 nextContinuations 中)。若有,取出第一个延续,恢复它并将结果直接传递。
  4. 用户迭代结果

    • 用户代码执行 for await result in group
    • 底层调用 group.next()
      a. 加锁,尝试从结果队列取一个结果。若有,立即返回。
      b. 若无结果,检查是否还有未完成子任务(childTasks 非空)。
      c. 若还有未完成子任务,则挂起当前任务,将其延续保存到 nextContinuations
      d. 若没有未完成子任务,返回 nil 结束迭代。
  5. 所有子任务完成

    • 最后一个子任务完成并调用 enqueue 后,GroupStorage 检测到 childTasks 变为空。
    • 如果有任何 nextContinuations 仍在等待,向它们发送 nil(结束信号)。
    • 设置 isComplete = true
  6. 离开作用域
    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 的现代并发模型代表了并发编程的范式转变:

  1. 从手动调度到智能调度 - 信任运行时做出最优决策
  2. 从回调地狱到线性代码 - 使用 async/await 简化异步流程
  3. 从容易出错到内存安全 - 通过 Actor 和值语义避免数据竞争
  4. 从复杂管理到结构化 - 自动处理任务生命周期和取消

虽然学习曲线比 GCD 更陡峭,但一旦掌握,你将能编写出更安全、更简洁、更高效的并发代码。


进一步学习资源: