GCD 中 dispatch_sync 和 dispatch_async 的区别?
在 GCD (Grand Central Dispatch) 中,dispatch_sync 和 dispatch_async 的核心区别在于 “是否等待任务执行完毕” 以及 “是否阻塞当前线程”。
简单总结:
- dispatch_sync (同步提交): “做完再走”。阻塞当前线程,直到 Block 中的任务执行完毕。
- dispatch_async (异步提交): “交给你了,我先走”。不阻塞当前线程,函数立即返回,任务在稍后执行。
以下是详细的对比分析:
1. 是否阻塞当前线程 (Blocking)
这是两者最本质的区别:
dispatch_sync:
- 阻塞。调用它的线程会被挂起(等待),直到被提交到队列的任务执行结束,
dispatch_sync函数才会返回,代码才会继续往下执行。 - 意味着:任务的执行顺序是确定的,必须等这一步做完才能做下一步。
- 阻塞。调用它的线程会被挂起(等待),直到被提交到队列的任务执行结束,
dispatch_async:
- 不阻塞。调用它的线程不会等待,
dispatch_async函数会立即返回,代码继续往下执行。被提交的任务会在未来的某个时间点由 GCD 安排执行。 - 意味着:你无法确定任务什么时候开始,也无法确定它什么时候结束(除非使用 Group 或 Semaphore)。
- 不阻塞。调用它的线程不会等待,
2. 是否开启新线程 (Threading)
虽然 sync 和 async 主要描述的是提交方式,但它们对线程的使用也有不同倾向:
dispatch_sync:
- 通常不开启新线程。为了优化性能,GCD 通常会直接在当前线程中执行该任务(即“借用”当前线程)。
- 例外:如果提交到主队列(Main Queue),它必须在主线程跑;如果提交到其他特定队列,逻辑上它是在那个队列跑,但物理上通常还是阻塞当前线程并利用当前线程执行。
dispatch_async:
- 具备开启新线程的能力。
- 如果提交到并发队列 (Concurrent Queue) 或 全局队列 (Global Queue),GCD 通常会从线程池中取出一个新线程来执行任务。
- 如果提交到主队列 (Main Queue),任务依然会在主线程执行(因为主队列是串行的且绑定主线程),但它会在下一次 RunLoop 循环中执行,不会卡住当前的执行流。
3. 常见场景与组合 (Queue + Dispatch)
理解它们的行为需要结合队列类型(串行 Serial / 并发 Concurrent):
| 组合方式 | 是否开启新线程 | 执行方式 | 备注 |
|---|---|---|---|
| Async + 并发队列 | 是 | 并发执行 | 最常用的后台任务处理方式(如下载、计算)。 |
| Async + 串行队列 | 是 (通常开启一条) | 串行执行 | 任务一个接一个执行,不阻塞当前线程。 |
| Async + 主队列 | 否 | 串行执行 | 最常用。用于从后台线程切回主线程刷新 UI。 |
| Sync + 并发队列 | 否 | 串行执行 | 在当前线程执行。常用于实现读写锁的“读”操作。 |
| Sync + 串行队列 | 否 | 串行执行 | 在当前线程执行。 |
| Sync + 主队列 | 否 | 死锁 (Deadlock) | 如果在主线程调用会死锁。 |
4. 致命陷阱:死锁 (Deadlock)
这是面试和开发中最重要的考点。
场景:在当前串行队列中,使用 dispatch_sync 向同一个串行队列提交任务。
最典型的例子(主线程死锁):
plaintext
override func viewDidLoad() {
super.viewDidLoad()
print("1")
// 主线程正在执行 viewDidLoad
// 主队列是串行队列
dispatch_sync(dispatch_get_main_queue()) {
print("2")
}
print("3")
}
结果:程序崩溃(死锁),只会打印 "1"。
原因分析:
viewDidLoad在主线程(主队列)执行。- 遇到
dispatch_sync,主线程被阻塞,等待 Block 中的任务执行完毕。 - Block 被提交到了主队列,准备执行。
- 但是,主队列是串行的(FIFO),它必须等当前正在执行的任务(即
viewDidLoad)执行完,才能执行下一个任务(即 Block)。 - 互相等待:主线程在等 Block 执行完(因为 sync),Block 在等
viewDidLoad执行完(因为串行队列)。导致死锁。
5. 总结与应用建议
使用
dispatch_async的场景:- 耗时操作(网络请求、图片处理、大文件读写)。
- UI 刷新(
DispatchQueue.main.async)。 - 不想卡住当前界面的时候。
使用
dispatch_sync的场景:- 非常少用,需谨慎。
- 必须等待某个任务有了结果才能继续往下走(且不想用回调闭包时)。
- 线程安全:用于在并发队列中实现同步读取数据(配合 Barrier 实现读写分离)。
- 注意:永远不要在主线程对主队列使用
sync。
一句话记忆:sync 是“我等你做完”,async 是“你去做吧,我不等”。