xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • Swift Concurrency 中的 Threads 与 Tasks

Swift Concurrency 中的 Threads 与 Tasks

Swift Concurrency 的引入彻底改变了我们编写异步代码的方式。它用更抽象、更安全的任务(Task)模型替代了传统的直接线程管理,旨在提高性能、减少错误并简化代码。理解线程(Threads)和任务(Tasks)之间的区别,是掌握现代 Swift 并发编程的关键。

1. 线程(Threads):系统级资源

线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。

1.1 线程的特点

  • 系统资源:线程由操作系统内核管理和调度,创建、销毁和上下文切换开销较大。
  • 并发执行:多线程允许程序中的多个操作并发(Concurrently) 执行, potentially improving performance on multi-core systems。
  • 传统痛点:
    • 高内存开销:每个线程都需要分配独立的栈空间等内存资源。
    • 上下文切换成本:当线程数量超过 CPU 核心数时,操作系统需要频繁切换线程,消耗大量 CPU 资源。
    • 优先级反转(Priority Inversion):低优先级任务可能阻塞高优先级任务的执行。
    • 线程爆炸(Thread Explosion):过度创建线程会导致系统资源耗尽、性能急剧下降甚至崩溃。

在 Grand Central Dispatch (GCD) 时代,开发者需要显式地将任务分发到主队列或全局后台队列,并时刻警惕这些线程管理问题。

2. 任务(Tasks):更高层次的抽象

Swift Concurrency 引入了 任务(Task) 作为执行异步工作的基本单位。一个任务代表一段可以异步执行的代码。

2.1 任务的特点

  • 异步工作单元:一个 Task 封装了一段异步操作的逻辑。
  • 不绑定特定线程:Task 被提交到 Swift 的协作式线程池(Cooperative Thread Pool) 中执行,由运行时系统动态地分配到任何可用的线程上,而不是绑定到某个特定线程。
  • 结构化并发:Task 提供了结构化的生命周期管理,包括取消、优先级和错误传播。子任务会继承父任务的优先级和上下文,并确保在其父任务完成之前完成。
  • 挂起与恢复:Task 可以在 await 关键字标记的挂起点(Suspension Point) 挂起,释放当前线程以供其他任务使用,并在异步操作完成后在某个线程上恢复执行(很可能不是原来的线程)。

2.2 任务的创建方式

Swift Concurrency 提供了几种创建任务的方式:

  1. Task 初始化器:最常用的方式,用于在非异步上下文中启动一个新的异步任务。

    Task {
        // 这里是异步上下文
        let result = await someAsyncFunction()
        print(result)
    }
  2. async let 绑定:允许同时启动多个异步操作,并稍后等待它们的结果。

    func fetchMultipleData() async {
        async let data1 = fetchData(from: url1)
        async let data2 = fetchData(from: url2)
        // 两个请求同时进行
        let results = await (data1, data2) // 等待两者完成
    }
  3. 任务组(Task Group):用于动态创建一组并发的子任务,并等待所有子任务完成。

    func processImages(from urls: [URL]) async throws -> [Image] {
        try await withThrowingTaskGroup(of: Image.self) { group in
            for url in urls {
                group.addTask { try await downloadAndProcessImage(from: url) }
            }
            // 收集所有子任务的结果
            return await group.reduce(into: []) { $0.append($1) }
        }
    }

3. Swift 的协作式线程池(Cooperative Thread Pool)

Swift Concurrency 的高效核心在于其协作式线程池。

3.1 工作原理

  • 线程数量固定:线程池创建的线程数量通常与当前设备的 CPU 物理核心数相同(例如,iPhone 16 Pro 是 6 核,则线程池大小约为 6)。这避免了过度创建线程。
  • 协作而非抢占:线程池中的线程不会像传统线程那样被操作系统强制抢占式调度。相反,任务需要主动协作(Cooperate),在适当的时机(即 await 挂起点)主动挂起,释放线程给其他任务使用。
  • 高效调度:运行时系统负责将大量的 Task 高效地调度到数量有限的线程上执行。当一个任务在 await 处挂起时,线程不会空等,而是立刻去执行其他已经就绪的任务。

3.2 挂起与恢复(Suspension and Resumption)

这是理解 Swift Concurrency 非阻塞特性的关键。

struct ThreadingDemonstrator {
    private func firstTask() async throws {
        print("Task 1 started on thread: \(Thread.current)")
        try await Task.sleep(for: .seconds(2)) // 🛑 挂起点
        print("Task 1 resumed on thread: \(Thread.current)")
    }

    private func secondTask() async {
        print("Task 2 started on thread: \(Thread.current)")
    }

    func demonstrate() {
        Task {
            try await firstTask()
        }
        Task {
            await secondTask()
        }
    }
}

可能的输出:

Task 1 started on thread: <NSThread: 0x600001752200>{number = 3, name = (null)}
Task 2 started on thread: <NSThread: 0x6000017b03c0>{number = 8, name = (null)}
Task 1 resumed on thread: <NSThread: 0x60000176ecc0>{number = 7, name = (null)}

解读输出:

  1. Task 1 开始在线程 3 上执行。
  2. 遇到 await Task.sleep时,Task 1 被挂起,线程 3 被释放。
  3. 运行时系统调度 Task 2 开始执行,它可能被分配到空闲的线程 8 上。
  4. 2 秒后,Task 1 的睡眠结束,变为就绪状态。运行时系统安排它恢复执行,但可能分配到了另一个空闲的线程 7 上。

这个过程完美展示了 Task 与 Thread 的“多对一”关系以及挂起/恢复机制如何实现线程的高效复用。

4. 与 Grand Central Dispatch (GCD) 的对比

虽然 GCD 非常强大且成熟,但 Swift Concurrency 在其基础上提供了更现代的抽象。

方面Grand Central Dispatch (GCD)Swift Concurrency
抽象核心队列(DispatchQueue)任务(Task)
线程模型动态创建线程,数量可能远超过 CPU 核心数,可能导致线程爆炸。协作式线程池,线程数 ≈ CPU 核心数,从根本上避免线程爆炸。
阻塞与挂起提交到队列的 Block 会阻塞底层线程(如果内部执行同步操作)。在 await 处挂起任务,释放底层线程,不会阻塞。
性能优秀,但线程过多时上下文切换开销大。更优,极少的线程处理大量任务,减少上下文切换,CPU 更高效。
语法与可读性基于闭包的回调,嵌套地狱(Callback Hell)风险。线性化的 async/await 语法,代码更清晰、更易读。
状态管理需要手动处理引用循环([weak self])。结构化并发减少了循环引用风险。
安全性需要开发者自己避免数据竞争(Data Race)。通过 Actor 和 Sendable 协议在编译时提供数据竞争安全。

4.1 性能对比:线程更少,性能更好?

这听起来有悖常理,但却是事实。GCD 的线程爆炸问题会导致内存压力增大和大量的上下文切换,反而消耗了 CPU 资源,使得真正用于执行任务的 CPU 周期减少。

Swift Concurrency 的协作式模型通过以下方式提升效率:

  • Continuations:挂起任务时,其状态(局部变量、执行位置等)被保存为一个 Continuation 对象。线程本身被释放,可以立即去执行其他任务。这比传统的线程阻塞和唤醒要轻量得多。
  • 始终前进:线程池中的线程几乎总是在执行有效工作,而不是空转或忙于切换。这使得单位时间内可以完成更多工作。

5. 常见误区与澄清

在从 GCD 转向 Swift Concurrency 时,需要扭转一些“线程思维”。

误区正解
每个 Task 都会创建一个新线程Task 与线程是多对一的关系。大量 Task 共享一个小的线程池。
await 会阻塞当前线程await 会挂起当前 Task,并释放当前线程供其他 Task 使用。这是非阻塞的。
Task 会按创建顺序执行Task 的执行顺序没有保证,取决于运行时系统的调度策略、优先级和挂起点。
必须在主线程上更新 UI✅ 正确。但在 Swift Concurrency 中,更推荐使用 @MainActor 来隔离 UI 相关代码,而不是手动派发到主队列。

6. 从“线程思维”到“任务思维”

开发者需要实现一个思维转变:

线程思维 (GCD Mindset)任务思维 (Task Mindset)
“这段重计算要放到后台线程。”“这段计算是个异步任务,系统会帮我调度。”
“完成后需要手动派发回主线程更新 UI。”“用 @MainActor 标记这个函数,确保它在主线程运行。”
“创建太多并发队列会不会导致线程爆炸?”“线程数量由系统自动管理,我只需专注业务逻辑和创建合理的 Task。”

7. 实践中的差异:Thread.sleep 与 Task.sleep

这个例子能深刻体现阻塞与挂起的区别。

  • Thread.sleep(forTimeInterval:):这是一个阻塞式调用。它会使当前所在的线程停止工作指定的时间。如果这个线程是协作线程池中的一员,它就相当于被“卡住了”,无法为其他任务服务,减少了有效工作线程数。
  • Task.sleep(for:):这是一个非阻塞式挂起。它会使当前 Task 挂起指定的时间,但当前任务所占用的线程会立刻被释放,并返回线程池中为其他就绪的 Task 服务。时间到后,Task 会被重新调度到某个可用线程上恢复执行。

结论:在 Swift Concurrency 中,绝对不要使用 Thread.sleep,它会破坏协作模型。始终使用 Task.sleep。

8. 如何选择:Swift Concurrency 还是 GCD?

尽管 Swift Concurrency 更现代,但 GCD 仍有其价值。

  • 使用 Swift Concurrency (Task) 当:
    • 项目基于 Swift 5.5+。
    • 想要更安全、更易读的异步代码(async/await)。
    • 希望获得更好的性能并避免线程问题。
    • 需要利用 Actor 等数据竞争安全特性。
  • 使用 Grand Central Dispatch (GCD) 当:
    • 维护旧的、大规模使用 GCD 的代码库,迁移成本高。
    • 需要进行非常底层的线程控制(虽然绝大多数场景不需要)。
    • 与某些高度依赖 GCD 的 C API 或旧框架交互。

混合使用:在实际项目中,两者可以共存。你可以在 Swift Concurrency 的 Task 内部使用 DispatchQueue 进行特定的操作,但要注意避免不必要的线程跳跃和性能损耗。

9. 深入底层:任务、作业与执行器(Tasks, Jobs, Executors)

为了更深入地理解,可以了解一些运行时概念:

  • 作业 (Job):任务是比 Task 更小的执行单位。一个 Task 在编译时会被分解成多个连续的 Job。每个 Job 是一个同步执行的代码块,位于两个 await 挂起点之间。Job 是运行时系统实际调度的单位。
  • 执行器 (Executor):是一个服务,负责接收被调度的 Job 并安排线程来执行它。系统提供了全局的并发执行器(负责一般任务)和主执行器(负责 @MainActor 任务)。开发者通常不需要直接与之交互。

总结

Swift Concurrency 中的 Threads 和 Tasks 是不同层次的概念:

  • Thread 是系统级的底层资源,由操作系统管理,创建和切换开销大。Swift Concurrency 建立在线程之上,但开发者不再需要直接与之交互。
  • Task 是语言级的高层抽象,代表一个异步工作单元。它帮助开发者摆脱繁琐且易错的线程管理,专注于业务逻辑。

Swift Concurrency 的核心优势在于其协作式线程池模型和挂起/恢复机制。它通过以下方式实现高效并发:

  1. 限制线程数量(与 CPU 核心数一致),避免线程爆炸。
  2. 使用 await 作为挂起点,任务在此主动释放线程,实现非阻塞。
  3. 利用 Continuations 保存挂起状态,实现任务在不同线程上的恢复。
  4. 通过 Actor 和结构化并发提供编译期的数据竞争安全。

最终,开发者应从“线程思维”转向“任务思维”,信任运行时系统会做出最优的线程调度决策,从而编写出更清晰、更安全、更高效的高并发代码。

最后更新: 2025/9/23 09:31