# 12.7.5 死锁

信号量引入了一种潜在的令人厌恶的运行时错误,叫做死锁 (deadlock),它指的是一组线程被阻塞了,等待一个永远也不会为真的条件。进度图对于理解死锁是一个无价的工具。例如,图 12-44 展示了一对用两个信号量来实现互斥的线程的进度图。从这幅图中,我们能够得到一些关于死锁的重要知识:

一个会死锁的程序的进度图

图 12-44 一个会死锁的程序的进度图

  • 程序员使用 P 和 V 操作顺序不当,以至于两个信号量的禁止区域重叠。如果某个执行轨迹线碰巧到达了死锁状态 d,那么就不可能有进一步的进展了,因为重叠的禁止区域阻塞了每个合法方向上的进展。换句话说,程序死锁是因为每个线程都在等待其他线程执行一个根本不可能发生的 V 操作。
  • 重叠的禁止区域引起了一组称为死锁区域 (deadlock region) 的状态。如果一个轨迹线碰巧到达了一个死锁区域中的状态,那么死锁就是不可避免的了。轨迹线可以进入死锁区域,但是它们不可能离开。
  • 死锁是一个相当困难的问题,因为它不总是可预测的。一些幸运的执行轨迹线将绕开死锁区域,而其他的将会陷入这个区域。图 12-44 展示了每种情况的一个示例。对于程序员来说,这其中隐含的着实令人惊慌。你可以运行一个程序 1000 次不出任何问题,但是下一次它就死锁了。或者程序在一台机器上可能运行得很好,但是在另外的机器上就会死锁。最糟糕的是,错误常常是不可重复的,因为不同的执行有不同的轨迹线。

程序死锁有很多原因,要避免死锁一般而言是很困难的。然而,当使用二元信号量来实现互斥时,如图 12-44 所示,你可以应用下面的简单而有效的规则来避免死锁:

互斥锁加锁顺序规则: 给定所有互斥操作的一个全序,如果每个线程都是以一种顺序获得互斥锁并以相反的顺序释放,那么这个程序就是无死锁的。

例如,我们可以通过这样的方法来解决图 12-44 中的死锁问题:在每个线程中先对 s 加锁,然后再对 t 加锁。图 12-45 展示了得到的进度图。

一个无死锁程序的进度图

图 12-45 一个无死锁程序的进度图

练习题 12.15 思考下面的程序,它试图使用一对信号量来实现互斥。

初始时:s = 1, t = 0

线程 1 线程 2
P(s); P(s);
V(s); V(s);
P(t); P(t);
V(t); V(t);

A. 画出这个程序的进度图。

B. 它总是会死锁吗?

C. 如果是,那么对初始信号量的值做哪些简单的改变就能消除这种潜在的死锁呢?

D. 画出得到的无死锁程序的进度图。