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

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

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

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

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 子任务‌。根据结构化并发的规则:

  1. 隐式取消 ‌:运行时会‌自动向 child1child2 发送取消信号‌。
  2. 隐式等待 ‌:父任务‌不会立即返回 ‌,而是隐式地 await 这两个子任务直到它们响应取消并结束。
  3. 抛出错误 ‌:如果 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 使用注意点

  1. await 不是线程切换await 只表示"可能挂起",恢复时不保证回到同一线程
  2. 不要在 deinit 中使用 asyncdeinit 是同步的,对象可能已释放
  3. async let 的生命周期 :必须在作用域结束前 await,否则任务取消但不会报错
  4. 重入问题actorasync 方法在 await 挂起后恢复时,隔离状态可能已变,这是 actor 重入的根源
  5. 死锁风险 :同步函数中无法调用 async 函数,必须通过 Task 包裹;但 TaskMainActor 上可能造成主线程等待

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

  1. 当前任务被挂起 (suspend),并暂时释放 Actor 的执行器
  2. 执行器立即从队列中取出下一个任务开始执行。
  3. 当被挂起的任务恢复时,它会重新进入队列末尾(或按优先级插队),等待再次获得执行权。
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")
    }
}
  • methodAmethodB 运行在同一个任务中,当前任务已经持有了该 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 letTaskGroup 中添加的任务,会自动继承父任务当前的 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 letTaskGroup 属于结构化并发(父子生命周期绑定)
协作式取消 取消是请求式 的,不是强制的;需要在任务内部主动检查 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 awaitnext() 按完成顺序处理结果,无需等待所有任务完成。

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 表示迭代结束。
  • 对于 withThrowingTaskGroupnext() 会抛出错误(如果子任务抛出),因此需要 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

  • 值类型structenum)且所有成员都是 Sendable
  • actor 类型自动符合 Sendable(因为它自身提供隔离)。
  • 不可变的类 (即所有存储属性是 let 且类型也是 Sendable)。
  • 某些标准库类型被显式标记为 @unchecked Sendable(如 IntStringArray<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`** 是同一运行时对象的两个名字,
分别强调其**行为**(状态机)和**存储**(帧数据)。

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

  1. 调用开始

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

  2. 执行到第一个 await

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

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

    await 的异步函数最终通过 continuation.resume()(或 .resume(throwing:))通知完成。

    resume 内部:

    • 将关联的 ExecutorJob 重新入队到原来的执行器(或根据 executorPreference 指定的执行器)。
    • 执行器稍后再次调用 job.run(),这次从上一个挂起点之后的地址继续执行。
  4. 恢复执行

    job.run() 被再次调用时,它从 AsyncFunctionFrame 中读取恢复地址和保存的局部变量,继续执行后续代码,直到遇到下一个 await 或函数返回。

  5. 函数返回

    当执行到 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)。对外提供 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 更陡峭,但一旦掌握,你将能编写出更安全、更简洁、更高效的并发代码。


进一步学习资源:

相关推荐
HouWan1 小时前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo2 小时前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记7 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程7 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne13 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记1 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩1 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年1 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008891 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
HouWan2 天前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios