这是一个非常本质、非常“根”的问题。
很多人停留在“async 不阻塞 / sync 阻塞”,但那只是表象。
先给一句可以直接当结论用的 👇
async和sync的本质区别不在“是否新线程”,而在“提交阶段是否把当前线程纳入调度依赖”。
下面分 提交阶段(enqueue) 和 执行阶段(execute) 拆清楚。
1️⃣ 先给一张“总览对照表”
| 维度 | async | sync |
|---|---|---|
| 提交是否立即返回 | ✅ | ❌ |
| 是否阻塞当前线程 | ❌ | ✅ |
| 当前线程是否参与执行 | ❌(通常) | ✅(可能) |
| 是否建立调度依赖 | ❌ | ✅ |
| 是否可能死锁 | 较少 | 很容易 |
2️⃣ 调度阶段的本质差异(最重要)
async:纯提交,不参与调度
queue.async {
work()
}
在调度层面:
-
当前线程:
- 只是把 block 放进 queue
- 立刻返回
-
从这一刻起:
- 当前线程 和这个 block 完全无关
-
block:
- 什么时候跑
- 在哪条线程跑
- 由队列 + 系统决定
👉 async 是:
“我把任务交给你了,你爱什么时候跑什么时候跑。”
sync:提交 + 等待 + 参与调度
queue.sync {
work()
}
在调度层面:
-
当前线程:
- 把 block 放进 queue
- 进入等待状态
-
并且:
- 当前线程 被视为该任务的潜在执行者
这点非常关键。
👉 sync 是:
“我把任务交给你,但我就在这等着,你要不就让我自己干,要不你安排别人干完再叫醒我。”
3️⃣ 执行阶段的差异(容易被忽略)
async 的执行阶段
-
block 一定在“别的时间”执行
-
几乎不可能:
- 在当前调用栈中执行
-
当前线程:
- 不会跑这个 block
sync 的执行阶段(⚠️重点)
block 有 两种可能执行方式:
① 当前线程直接执行(fast-path)
queue.sync {
work()
}
如果:
- queue 是空的
- 没有顺序冲突
- 当前线程 QoS 合适
👉 block 会直接在当前线程执行
这也是为什么:
print("A")
queue.sync { print("B") }
print("C")
输出一定是:
A
B
C
而且 B 在同一线程。
② 其他线程执行,当前线程等待
如果:
- queue 忙
- 或当前线程不适合执行(QoS、绑定限制)
那么:
-
block 被派发到其他 worker thread
-
当前线程:
- 阻塞
- 等待完成信号
4️⃣ 死锁为什么几乎只和 sync 有关?
经典例子:
DispatchQueue.main.sync {
print("boom")
}
发生了什么?
-
主线程:
- sync 提交任务到 main queue
- 进入等待
-
main queue:
- 只能由主线程执行
-
结果:
- 互相等待 → 死锁
👉 根因不是“主线程特殊”,而是:
sync 把“当前线程”加入了调度依赖链
async 不会这么做。
5️⃣ 一个容易忽略的关键点
sync ≠ 一定“不开新线程”
很多人以为:
sync 就是不新建线程,async 才新建线程
这是 完全错误的模型。
正确模型是:
-
async / sync 都不控制线程创建
-
它们只控制:
- 当前线程是否等待
- 是否允许当前线程执行任务
6️⃣ 用一句话区分它们(工程直觉版)
- async:
👉 fire-and-forget(发射完就走) - sync:
👉 submit-and-join(提交并加入执行 / 等待)
7️⃣ 一句话终极总结(请背)
async只是把任务加入队列并立即返回;sync不仅加入队列,还让当前线程参与到该任务的调度与完成依赖中,这也是它会阻塞、可能复用当前线程、并引发死锁的根本原因。
👉 追问:sync 为什么会阻塞当前线程?
sync会阻塞当前线程,是因为它的语义不是“提交任务”,而是“在该队列的执行序中完成这段代码后才返回”,这在实现上只能通过让当前线程等待(或直接执行)来保证。
下面把这个“为什么只能这样做”拆清楚。
1️⃣ 先看 sync 的语义承诺(不是实现)
当你写:
queue.sync {
work()
}
你向 GCD 要求的是:
在 queue 的顺序语义中,等这段代码执行完成之后,当前调用点才能继续。
注意关键词:
- 顺序语义
- 完成之后才返回
这意味着:
- 不能提前返回
- 不能“过一会儿再回来”
- 必须在这里等结果
👉 这和 async 的承诺完全不同。
2️⃣ 那为什么“一定要阻塞当前线程”?
因为只有三种可能性,我们逐个排除。
❌ 方案一:返回一个 future / callback(不可行)
如果 sync 不阻塞:
let result = queue.sync { work() } // 立刻返回?
那就变成:
- 要么返回占位符
- 要么注册回调
这已经不是 sync 了,而是:
- async + future
- async + await
👉 违背 API 语义
❌ 方案二:偷偷在后台执行、假装同步(不成立)
如果:
- block 在后台执行
- 当前线程继续跑
那就无法保证:
- block 已经完成
- 后续代码 看到的是正确状态
这会直接破坏:
- 串行队列的顺序一致性
- 同步调用的可预测性
✅ 方案三:当前线程等待(或直接执行)——唯一解
所以 GCD 只能:
-
把 block 放进队列
-
然后:
- 阻塞当前线程
- 直到 block 执行完成
这不是实现选择,而是:
由 sync 的“完成即返回”语义强制决定的
3️⃣ 那为什么有时看起来“没有阻塞”?
因为有 fast-path:
queue.sync {
work()
}
如果:
- queue 当前是空的
- 当前线程允许执行该队列的任务
GCD 会:
- 直接在当前线程执行 block
- 执行完立刻返回
看起来像是“没阻塞”,但本质是:
当前线程已经把工作做完了,自然不用等
语义上仍然是“同步完成后返回”。
4️⃣ 阻塞不是“浪费”,而是同步屏障
从调度角度看:
-
sync 建立了一条 happens-before 关系
-
保证:
- queue 中在它之前的任务已完成
- 当前 block 已完成
- 后续代码看到的是稳定状态
这就像:
- 内存屏障
- 或 join 一个线程
5️⃣ 为什么 async 不会阻塞?
因为 async 的语义是:
“我不关心你什么时候跑,我先走了。”
它不要求:
- 顺序完成
- 立即可见结果
所以:
- 不建立完成依赖
- 不阻塞线程
6️⃣ 经典死锁,其实就是“阻塞用错地方”
DispatchQueue.main.sync {
print("boom")
}
发生了什么?
-
主线程:
- sync → 阻塞
-
main queue:
- 只能由主线程执行
-
结果:
- 没有任何线程能执行 block
- 永远等不到完成
👉 不是 sync “坏”,而是:
在“唯一执行者”上使用 sync 是逻辑矛盾
7️⃣ 和其他同步机制的对比
| 机制 | 是否阻塞 | 本质 |
|---|---|---|
| DispatchQueue.sync | ✅ | 等队列完成 |
| NSLock.lock | ✅ | 等锁 |
| DispatchSemaphore.wait | ✅ | 等信号 |
| await | ❌(挂起) | 等结果但不占线程 |
这也解释了:
- 为什么 Swift 引入
await - 而不是再加一个“非阻塞 sync”
🔚 一句话终极总结
sync阻塞当前线程不是实现细节,而是其“必须在队列顺序中完成后才能返回”的语义所必然要求;阻塞(或直接执行)是唯一能满足该语义的实现方式。