Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

死锁是怎样形成的

复习

  • 生产者与消费者:用互斥和等待通知串起一个完整例子
  • 信号量:用计数管理有限资源与线程协作
  • 读者与写者:并发读取时如何协调写入与公平性

TL;DR

  • 两个线程各持一把锁,又都去等对方手里的锁,就会死锁
  • 死锁的四个必要条件:互斥、持有并等待、不可抢占、循环等待
  • 这四个条件同时满足,才可能死锁;破坏任一个,就能避免
  • 死锁不一定每次出现,取决于时序,因此更隐蔽

正文

  并发里最让人头疼的,不是算错,而是大家全停下、谁也不动。这叫死锁(deadlock)。

一个经典的僵局

  假设有两把锁 AB,两个线程各自这样写:

线程 1:          线程 2:
lock(A)           lock(B)
lock(B)           lock(A)
...

  如果刚好这样执行:

线程 1:拿到 A
线程 2:拿到 B
线程 1:想要 B —— 但 B 在别人手里,等
线程 2:想要 A —— 但 A 在别人手里,等

  于是:线程 1 攥着 A 等 B,线程 2 攥着 B 等 A,谁也不肯先放手。两个线程永远卡在原地,程序就此僵住。这就是死锁。

四个必要条件

  死锁的发生,需要同时满足四个条件:

  1. 互斥:资源同一时刻只能被一个线程占用
  2. 持有并等待:拿着手里的资源不放,同时去申请别的
  3. 不可抢占:资源不能被强行夺走,只能由持有者主动释放
  4. 循环等待:存在一条“你等我、我等你”的环形等待链

  上面那个例子,四个条件全占了:A、B 互斥;两个线程都持有并等待;锁不能被抢;等待链形成了环(1 等 2、2 等 1)。

为什么它难缠

  关键在于:四个条件同时满足才可能死锁,而且往往还要凑巧的时序。大多数时候,线程 1 先拿完两把锁再轮到线程 2,一切正常;只有刚好交错到那个坏顺序,才会卡死。

  于是死锁又成了一个“幽灵问题”:测试时好好的,偶尔才复现一次,还极难定位。

出路在哪

  不过,这四个条件也给了我们抓手:只要破坏其中任意一个,死锁就不可能发生。 剩下来的问题就是——怎么破坏、破坏哪一个,代价又有多大。这正是下一章要讨论的。

思考题

  死锁的四个必要条件里,哪些是“资源本身的性质”(比如锁天生互斥),哪些是“程序写法造成的”?如果只能改程序,你会先动哪一个?

小结

知识点

  • 死锁:互相持有并等待对方资源,导致全员停滞
  • 四个必要条件:互斥、持有并等待、不可抢占、循环等待
  • 四者同时满足才可能死锁,破坏其一即可避免
  • 死锁依赖时序,难以复现

参考资料

  1. Wikipedia(zh):死锁:deadlock
  2. Wikipedia(zh):死锁的必要条件:四个条件

思考题答案(仅供参考)

  “互斥”多半是资源本身的性质——像一把锁,本质就是同一时刻只许一个;“不可抢占”也有很强的硬件/语义色彩,锁通常不能被别人强行夺走。“持有并等待”和“循环等待”则主要是程序写法造成的:如果每个线程都一次把需要的锁全申请好,或者大家都按同一顺序申请,环就凑不出来。所以最容易动手的,是先破坏“循环等待”(比如统一加锁顺序),这也是下一章最推荐的做法。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪