原文: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:
Task 2 started on thread:
Task 1 resumed on thread:
解读:
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 就行。"