4-29.【OC】【锁】主线程同步派发到主队列为什么必死锁?

4 阅读3分钟

这是一个非常经典且容易在面试中遇到的问题。在 iOS 开发中,执行 dispatch_sync(dispatch_get_main_queue(), ^{ ... }); 会产生立即且永久的死锁。

这种死锁的本质并不是资源竞争,而是由于串行队列的 FIFO(先进先出)特性与线程阻塞导致的“环路等待”

1. 核心原因:环路等待图 (Wait-for Graph)

我们可以从“队列”和“线程”两个维度来分析这个环路:

  1. 主线程(Thread)在等任务结束dispatch_sync 是一个同步调用,它的特性是:阻塞当前线程,直到被派发的任务块(Block)执行完毕。因此,主线程现在停在 dispatch_sync 这一行,等待 Block 完成。
  2. 任务块(Block)在等主线程执行: 主队列(Main Queue)是一个串行队列,遵循 FIFO 原则。这意味着队列里的任务必须按顺序一个一个执行。 由于主队列的任务是在主线程上运行的,而此时主线程正被 dispatch_sync 阻塞着,无法处理队列中的新任务。
  3. 死锁形成: Block 被排在主队列的队尾,它在等待主线程空闲来执行它;而主线程却在等待 Block 执行完才肯继续工作。

2. 逻辑拆解

我们可以把主队列想象成一个单行道(串行队列),主线程是这条路上唯一的车。

  • 第一步:主线程正在执行当前的函数。
  • 第二步:执行到 dispatch_sync,它把一个新任务 Task B 放到了单行道的后面。
  • 第三步dispatch_sync 告诉主线程:“你先别动,等 Task B 跑完你再走。”
  • 第四步:主线程停下来了。但是,Task B 在主线程后面,它心想:“前面的车(主线程当前任务)不走完,我永远没法开始跑啊!”

结果:前面的车在等后面的车跑完,后面的车在等前面的车走开。路彻底堵死了。


3. 特殊情况:为什么 dispatch_async 不会死锁?

如果换成 dispatch_async

  • 它会把 Block 放入主队列队尾,但它不会阻塞当前线程,函数会立即返回。
  • 主线程继续执行完当前代码块,回到了 RunLoop。
  • RunLoop 发现队列里有新任务(刚才异步派发的 Block),于是取出并执行。
  • 这就避开了“互相等待”的闭环。

4. 延伸:非主队列的 sync 会死锁吗?

死锁的条件是**“在当前队列中同步派发任务到当前队列”**。

  • 会死锁:在私有串行队列 queueA 中执行 dispatch_sync(queueA, ^{ ... });
  • 不会死锁:在主线程中同步派发到一个私有并发队列(Concurrent Queue),或者派发到另一个不同的串行队列

关联到您的 slowlog_0

在您的日志中,主线程出现了严重的卡顿(Lag)。虽然日志中没有直接显示 dispatch_sync 导致的硬死锁(硬死锁会导致应用直接卡死无响应并被系统 Watchdog 杀掉),但很多由于锁竞争主线程承载过多同步任务导致的现象,在表现上非常接近这种“逻辑卡死”。

例如,个推 SDK 如果在内部不小心在主线程同步等待了某个只有在主线程才能完成的回调,就会造成类似的阻塞。

总结一句话: 永远不要在串行队列中同步派发任务给该队列自身,这等同于“自己等自己”。