原文:Threads vs. Tasks in Swift Concurrency 链接:www.avanderlee.com/concurrency...
前言:别再问"它跑在哪个线程?"
在 GCD 时代,我们习惯用 DispatchQueue.global(qos: .background).async { ... }
或 DispatchQueue.main.async { ... }
来显式地把任务丢到指定线程。久而久之,形成了一种"线程思维":
"这段代码很重,我要放到子线程。"
"这行 UI 代码必须回到主线程。"
Swift Concurrency(async/await
+ Task
)出现以后,这套思维需要升级------系统帮你决定"跑在哪个线程"。我们只需关心"任务(Task)"本身。
线程(Thread)到底是什么?
- 系统级资源:由操作系统调度,创建、销毁、切换开销大。
- 并发手段:多线程可以让多条指令流同时跑。
- 痛点:数量一多,内存占用高、上下文切换频繁、优先级反转。
Swift Concurrency 的目标就是让我们 不再直接面对线程。
Task:比线程更高级的抽象
- 一个 Task = 一段异步工作单元。
- 不绑定线程:Task 被放进 合作线程池(cooperative thread pool),由运行时动态分配到"刚好够用"的线程上。
- 运行机制:
- 线程数量 ≈ CPU 核心数。
- 遇到
await
(挂起点)时,当前线程被释放,可立即执行其他 Task。 - 挂起的 Task 稍后可能在另一条线程恢复。
代码示范:Task 与线程的"若即若离"
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 在
await
时释放了线程 3; - Task 2 趁机用到了线程 8;
- Task 1 恢复时,被安排到线程 7------前后线程可以不同。
线程爆炸(Thread Explosion)还会发生吗?
场景 | GCD | Swift Concurrency |
---|---|---|
同时发起 1000 个网络请求 | 可能创建 1000 条线程 → 内存暴涨、调度爆炸 | 最多 CPU 核心数条线程,其余任务挂起 → 无爆炸 |
阻塞线程 | 线程真被 block,CPU 空转 | 用 continuation 挂起,线程立刻服务别的任务 |
因此,线程爆炸在 Swift Concurrency 中几乎不存在。
线程更少,性能反而更好?
- GCD 误区:线程越多,并发越高。
- 真相:线程 > CPU 核心时,上下文切换成本激增。
- Swift Concurrency 做法 :
- 线程数 = 核心数;
- 用挂起/恢复代替阻塞;
- CPU 始终在跑有效指令,切换开销极低。
实测常见场景(CPU-bound & I/O-bound)下,Swift Concurrency 往往优于 GCD。
三个常见误区
误区 | 正解 |
---|---|
每个 Task 会新开一条线程 | Task 与线程是多对一,由调度器动态复用 |
await 会阻塞当前线程 |
await 会挂起任务并释放线程 |
Task 一定按创建顺序执行 | 执行顺序不保证,取决于挂起点与调度策略 |
思维升级:从"线程思维"到"任务思维"
线程思维 | 任务思维 |
---|---|
"这段代码要在子线程跑" | "这段代码是异步任务,系统会调度" |
"回到主线程刷新 UI" | "用 @MainActor 或 MainActor.run 标记主界面任务" |
"我怕线程太多" | "线程数系统自动管理,我专注业务逻辑" |
小结
- 线程是低层、昂贵的系统资源。
- Task 是高层、轻量的异步工作单元。
- Swift Concurrency 通过合作线程池 + 挂起/恢复机制,让线程数始终保持在"刚好够用",既避免线程爆炸,又提升性能。
- 开发者应把注意力从"线程"转向"任务"与"挂起点"。
当你下次再想问"这段代码跑在哪个线程?"时,提醒自己:
"别管线程,写正确的 Task 就行。"