不一定,而且这是 GCD 里最容易被误解的一点之一。
先给一句“反直觉但正确”的结论👇
串行队列保证的是“同一时间只执行一个任务”,而不是“永远只使用同一个线程”。
下面把 为什么、怎么做到的、什么时候看起来像一个线程 全讲清楚。
1️⃣ 串行队列真正的承诺是什么?
❌ 它不承诺
- 只用一个线程
- 任务都在同一条线程上跑
✅ 它只承诺
- FIFO 顺序
- 任意时刻最多只有一个 block 在执行
也就是说:
task A ──▶ 运行中
task B ──▶ 等待
task C ──▶ 等待
而不是:
thread 1: A
thread 1: B
thread 1: C
2️⃣ 那为什么“看起来”常常只用一个线程?
因为 很多时候“没必要换线程” 。
举个例子:
let serial = DispatchQueue(label: "serial")
serial.async { print(Thread.current) }
serial.async { print(Thread.current) }
很可能你看到:
<Thread: 0x...>{number = 5}
<Thread: 0x...>{number = 5}
这是因为:
- 第一个任务执行完
- 线程还空闲
- GCD 直接复用它
- 省得切换线程(更快)
👉 但这是优化结果,不是语义保证。
3️⃣ 什么时候会用到“不同线程”?(关键)
情况 1:线程被阻塞 / 挂起
serial.async {
sleep(2)
}
serial.async {
print("next")
}
-
第一个 block 睡眠
-
原线程被占用
-
GCD 可能:
- 让第二个 block
- 在 另一个 worker thread 上跑
-
但仍然保证:
- 等第一个完成后才执行
情况 2:QoS / 优先级变化
serial.async {
DispatchQueue.global(qos: .userInteractive).sync {
// 高 QoS
}
}
-
当前线程 QoS 可能不合适
-
GCD 可能切换到:
- 更高优先级的线程
-
继续执行后续 block
情况 3:线程回收 / 系统压力
- 系统回收空闲线程
- 下一个 block 到来
- 重新分配一个新线程
串行队列对此完全不关心。
4️⃣ 那 GCD 是怎么保证“不会并行”的?
🔒 靠的是队列内部状态,不是线程锁
对于串行队列:
-
队列维护一个状态:
- “是否正在执行任务”
-
每次:
- 只允许一个 block 被派发到 target queue
-
直到:
- 当前 block 完成
- 才派发下一个
即使 target queue 是并发的:
- 这个限制仍然成立
👉 串行是调度规则,不是执行资源限制
5️⃣ 和主队列的区别(容易混)
主队列为什么“像是一个线程”?
因为:
- 主队列 target = 主线程
- 主线程只有一个
- RunLoop 串行执行
所以主队列是:
串行队列 + 单一执行线程
但这只是一个特殊情况。
6️⃣ 一个常见错误假设(⚠️)
“串行队列 = 线程安全”
❌ 错。
let serial = DispatchQueue(label: "serial")
var value = 0
DispatchQueue.global().async {
value += 1
}
serial.async {
print(value)
}
-
serial 只能保证:
- serial 内部的任务不并发
-
并不能阻止:
- 其他队列并发访问同一资源
线程安全 ≠ 串行队列。
7️⃣ 一句话终极总结(请背)
串行队列保证的是执行的顺序性和互斥性,而不是线程的唯一性;GCD 可以在不同时间使用不同线程来执行串行队列的任务,只要不破坏“同时只有一个任务在运行”的约束。
👉 追问:串行队列中的任务可能运行在不同线程上吗?
可以,而且这是完全合法、也是 GCD 的常态行为。
一句话版结论先放这儿 👇
串行队列只保证“同一时间只有一个任务在执行”,并不保证“所有任务都在同一条线程上运行”。
下面把这个说清楚,并顺手把常见误区一次性扫掉。
1️⃣ 为什么“可以在不同线程上”?
因为在 GCD 里:
-
队列 ≠ 线程
-
串行队列只是一个调度约束
-
真正执行任务的是:
- target queue
- 共享线程池里的 worker thread
只要满足:
前一个任务已经结束,下一个任务就可以用任何可用线程来跑
2️⃣ 一个最直观的例子
let serial = DispatchQueue(label: "com.example.serial")
serial.async {
print("A:", Thread.current)
}
serial.async {
print("B:", Thread.current)
}
你可能看到:
A: <Thread 0x...>{number = 5}
B: <Thread 0x...>{number = 7}
也可能看到:
A: <Thread 0x...>{number = 5}
B: <Thread 0x...>{number = 5}
两种结果都 100% 正确。
3️⃣ GCD 是怎么保证“不会并发”的?
重点来了:
不是靠“只用一个线程” 。
而是靠 队列状态机:
-
串行队列内部维护一个状态:
isExecuting
-
当
isExecuting == true:- 不会把下一个任务派发出去
-
当前任务完成后:
isExecuting = false- 才允许派发下一个
👉 所以:
- 线程可以换
- 同一时刻只能有一个在跑
4️⃣ 什么情况下更容易“换线程”?
🧩 情况 1:线程被阻塞或让出 CPU
serial.async {
sleep(1)
}
serial.async {
print("next")
}
-
第一个任务睡眠
-
原线程被占用
-
下一个任务:
- 可能由另一个 worker thread 执行
🧩 情况 2:QoS / 优先级变化
-
前一个 block 是
.utility -
后一个 block 是
.userInitiated -
GCD 可能:
- 换到更高优先级的线程
🧩 情况 3:线程回收 / 系统压力
- 线程被系统回收
- 下一个任务到来
- 分配新线程
5️⃣ 唯一的“例外”:主队列
DispatchQueue.main.async {
// 一定在 main thread
}
- 主队列 = 串行
- target = 主线程
- 主线程只有一个
所以:
主队列的串行 + 单线程是“特例”,不是串行队列的通性
6️⃣ 一个非常容易踩的坑(⚠️)
❌ 错误假设
“我在串行队列里存 Thread.local / TLS,一定安全”
Thread.current.threadDictionary["key"] = value
如果任务换线程:
- thread-local 数据就丢了
- 行为会非常诡异
👉 串行队列 ≠ 线程局部环境
7️⃣ 正确心智模型(请记住)
-
串行队列:
- 保证 顺序
- 保证 互斥
-
线程:
- 是 执行资源
- 可被复用、替换、提升优先级
🔚 一句话终极总结
串行队列中的任务完全可能运行在不同线程上;GCD 只保证这些任务不会并发执行,而不保证它们使用同一个线程。