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 提供了几种创建任务的方式:
Task
初始化器:最常用的方式,用于在非异步上下文中启动一个新的异步任务。
swift
Task {
// 这里是异步上下文
let result = await someAsyncFunction()
print(result)
}
async let
绑定:允许同时启动多个异步操作,并稍后等待它们的结果。
swift
func fetchMultipleData() async {
async let data1 = fetchData(from: url1)
async let data2 = fetchData(from: url2)
// 两个请求同时进行
let results = await (data1, data2) // 等待两者完成
}
- 任务组(Task Group):用于动态创建一组并发的子任务,并等待所有子任务完成。
swift
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 非阻塞特性的关键。
swift
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()
}
}
}
可能的输出:
arduino
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)}
解读输出:
-
Task 1 开始在线程 3 上执行。
-
遇到
await Task.sleep
时,Task 1 被挂起 ,线程 3 被释放。 -
运行时系统调度 Task 2 开始执行,它可能被分配到空闲的线程 8 上。
-
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 的核心优势在于其协作式线程池 模型和挂起/恢复机制。它通过以下方式实现高效并发:
-
限制线程数量(与 CPU 核心数一致),避免线程爆炸。
-
使用
await
作为挂起点,任务在此主动释放线程,实现非阻塞。 -
利用 Continuations 保存挂起状态,实现任务在不同线程上的恢复。
-
通过 Actor 和结构化并发提供编译期的数据竞争安全。
最终,开发者应从"线程思维"转向"任务思维",信任运行时系统会做出最优的线程调度决策,从而编写出更清晰、更安全、更高效的高并发代码。