iOS开发基础93-经典死锁:主线程 sync 主队列
iOS 经典死锁:主线程 sync 主队列
题目
- (void)interview01 {
// 以下代码是在主线程执行的
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
问:打印结果是什么?
答案
只打印 "执行任务1",然后程序死锁卡死,"执行任务2"和"执行任务3"永远不会打印。
一句话原理
主线程被 dispatch_sync 阻塞了,在等主队列里的 block 执行;但主队列是串行队列,block 要在主线程执行,而主线程被阻塞了没空执行——两边互相等,死锁。
深入分析
第一步:执行任务1
代码在主线程执行,NSLog(@"执行任务1") 正常打印。
第二步:dispatch_sync 主队列
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
这里发生了两件事:
dispatch_sync是同步派发:它会阻塞当前线程(主线程),等到 block 执行完成后才返回。- 主队列是串行队列:主队列的任务必须在主线程上一个一个顺序执行。
此时的状态:
主线程:正在执行 interview01,被 dispatch_sync 阻塞,等待 block 完成
主队列:排队等待执行 block,但主线程被占了,block 无法执行
第三步:死锁形成
- 主线程在等:block 执行完 → dispatch_sync 返回 → 继续执行任务3
- block 在等:主线程空闲 → 才能执行 → 打印任务2
主线程等 block,block 等主线程,循环等待,死锁。
图示
┌─────────────────────────────┐
│ 主线程(被阻塞) │
│ 执行 interview01 │
│ → dispatch_sync 等待... │
└──────────┬──────────────────┘
│ 等待 block 完成
▼
┌─────────────────────────────┐
│ 主队列(串行队列) │
│ [interview01 正在执行] │
│ [block 等待执行] ← 排队中 │
└──────────┬──────────────────┘
│ 需要主线程执行
▼
回到主线程(被阻塞)
↑ 循环等待,死锁
死锁的四个必要条件
这个经典案例同时满足了死锁的四个必要条件:
| 条件 | 对应情况 |
|---|---|
| 互斥 | 主线程同一时间只能执行一个任务 |
| 持有并等待 | 主线程持有执行权,同时等待 block 完成 |
| 不可剥夺 | dispatch_sync 不会被强制中断 |
| 循环等待 | 主线程等 block,block 等主线程 |
如何修改避免死锁
方案一:用 dispatch_async(异步)
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
dispatch_async 不阻塞当前线程,直接返回。打印顺序:任务1 → 任务3 → 任务2。
方案二:用全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_sync(queue, ^{
NSLog(@"执行任务2"); // 在子线程执行,不阻塞主线程
});
NSLog(@"执行任务3");
全局队列的任务在子线程执行,不会和主线程抢资源。打印顺序:任务1 → 任务2 → 任务3。
方案三:直接执行(本来就在主线程)
NSLog(@"执行任务2"); // 直接调用,不需要派发
NSLog(@"执行任务3");
既然已经在主线程了,直接执行代码就行,不需要再派发到主队列。
常见变体
变体一:串行队列 sync 自己
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{ // 在串行队列中 sync 同一个队列 → 死锁
NSLog(@"任务");
});
});
同样死锁:串行队列在执行外层 block,sync 派发到同一个队列,外层 block 等内层完成,内层等外层执行完才能执行。
变体二:并发队列 sync 自己(不会死锁)
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
dispatch_sync(concurrentQueue, ^{ // 并发队列可以同时执行多个任务 → 不会死锁
NSLog(@"任务");
});
});
并发队列可以同时执行多个任务,内层 block 可以在另一个线程执行,不会死锁。
变体三:主队列 async 主队列(不会死锁)
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
async 不阻塞,block 排队等主线程空闲后执行。打印:任务3 → 任务2。
Swift 版本对照
// 死锁版本(同样会死锁)
DispatchQueue.main.sync {
print("执行任务2") // 永远不会执行
}
// 正确写法一:async
DispatchQueue.main.async {
print("执行任务2")
}
// 正确写法二:全局队列
DispatchQueue.global().sync {
print("执行任务2") // 在子线程执行
}
// 正确写法三:直接执行(已经在主线程)
print("执行任务2")
常见问题
Q1:为什么 dispatch_sync 会阻塞?
dispatch_sync 的设计就是"同步执行"——提交任务后,当前线程停下来等任务执行完才继续。这和 dispatch_async(提交后立刻返回,不等待)相对。
Q2:主队列和主线程是什么关系?
- 主线程:一个线程,执行代码的实体。
- 主队列:一个串行队列,绑定到主线程,主队列的所有任务都在主线程执行。
- 主队列的任务必须排队,一个执行完才能执行下一个。
Q3:怎么快速判断会不会死锁?
口诀:在同一个串行队列中,sync 派发到同一个队列,必死锁。
判断步骤:
- 当前在哪个队列执行?
- sync 派发到哪个队列?
- 如果是同一个串行队列 → 死锁。
- 如果是并发队列或不同队列 → 不死锁。
Q4:死锁后程序会崩溃吗?
不会崩溃,而是主线程被永久阻塞,UI 无法响应,看起来像卡死了。Xcode 中暂停可以看到主线程停在 dispatch_sync 调用处。
总结
| 问题 | 答案 |
|---|---|
| 打印结果 | 只打印"执行任务1",然后死锁 |
| 死锁原因 | 主线程被 sync 阻塞,等主队列 block;主队列 block 等主线程空闲,互相等待 |
| 核心原因 | 串行队列中 sync 自己 |
| 解决方法 | 用 async、换全局队列、或直接执行 |
| 判断口诀 | 同一串行队列 sync 自己 → 死锁 |
将来的你会感谢今天如此努力的你!
版权声明:本文为博主原创文章,未经博主允许不得转载。

浙公网安备 33010602011771号