简单聊聊死锁那些事

时间:2021-11-30 20:50:46

前言


并发计算理论知识内,死锁是一个经常被拿来谈论的话题。今天,笔者再来也来简单聊聊死锁,死锁在多进/线程操作中是怎么触发的,如果发生了,我们有什么办法解决呢?下面笔者计划从死锁的定义(发生),死锁的预防,最后到死锁的避免3个模块来重新聊聊死锁。

死锁的定义


笔者在这里举一个生活中的一个常见例子:比如有4辆车,分别从四个方向(东、西、南、北)开来,最后一起开到了一个十字路口上,而且这时每辆车都打算往它们的右方向转弯,然后就会出现一个很有意思的现象,每辆车都被堵住了,因为它们谁都移动不了。这在某种程度上来说也是一种“死锁”。在这种情况下,一个简单的处理办法,就是让某一辆车先腾出位置,然后先让别的车过去,然后自己在过去,大意就是让外界因素人工干预一下,改变当前阻塞注的场景。

通过上述的这个例子,我们可以定义出死锁的概念:一组相互竞争资源的进程间的永久阻塞。这句话源自于操作系统设计与原理一书。图形展示效果如下。


简单聊聊死锁那些事

死锁发生的条件


很多人可能会有一个误区:只要是多线程操作,就一定会发生死锁。其实不是这样的,死锁只有在满足以下特定的场景要求下,才有可能发生。

第一,互斥。这里的互斥,指的是资源使用的互斥。一次只有一个进程可以使用一个资源。

第二,占有且等待。当一个进程等待其它进程释放资源时,会依然继续占有已分配给自己的资源。千万不要以为这是理所当然的行为,因为有些资源使用策略会要求先释放掉自己的资源才能再请求其它资源

第三,不可抢占。不能强行抢占其它进程已占有的资源。

第四,循环等待。循环等待其实在某种程度上来说更像是前面3个条件下的一个潜在结果。

换句话说,只有满足了前面这4个条件,就会存在发送死锁的可能性。

死锁的预防


在正常情况下,我们一般都不希望死锁发生,因为那会让问题变得非常的棘手。一种解决死锁的简单办法就是事前预防。预防的手段是破坏上节中我们提到的4个发生条件,让其中某些条件不能发生即可。下面且看笔者一一道来。

第一个条件,互斥。这个我们要想达到资源的不互斥好像有点难度,因为很多时候,我们是需要资源的互斥的使用的,比如说文件的写操作。这个条件我们没法改变。

第二个条件,占有且等待。这个我们可以用另外一种策略,进程一次性请求所有需要的资源,阻塞等待,直到所有需要请求的资源都能分配到。这种方式有一个很大的缺点是,它比较低效,比如说我请求的某些资源只是使用了一小会,绝大部分时间都处于无人使用,但是也不得不等进程运行完了被释放。在此期间,别的进程还要阻塞的等待这个资源。

第三个条件,不可抢占。不可抢占的条件可以有以下2种调整策略:

  • 如果一个进程请求其它资源时被拒绝,则它自身必须释放自身占有的资源。
  • 如果一个进程需要的资源被其他进程占有时,可以要求占有的进程释放资源,这种方式就是抢占式的了。在考虑进程优先级的时候,这种方案将是不错的选择。

第四个条件,循环等待。循环条件可以通过定义资源使用的线性顺序来预防。比如说我们只允许进程请求它自身占有资源’之后’的资源。如何理解这里的之后呢?这需要我们为每一种资源排个序,列个下标。

比如2个资源Ri和Rj,i < j 。如果2个进程A和B分别占据Ri和Rj,然后他们都相互请求对方的资源,此时死锁并不会发生,因为进程A占据着Ri,不能请求Rj,因为Rj资源在Ri前面。但是这种方法在有的时候也可能比较低效。

死锁的避免


下面我们来看看死锁的避免,问题来了:死锁避免看上去与死锁预防在语义上非常类似,二者有什么区别呢?

死锁的预防是通过至少破坏死锁发生的4个条件中的任何一个条件来达到不发生死锁的目的。而死锁避免则允许死锁发生条件的执行,但是会通过额外的操作执行,来避免死锁的发生。

以下是一种死锁避免的办法:

如果一个进程新增的一个资源请求会导致死锁的发生,则拒绝该请求并不分配资源给该进程。

那么系统如何知道这里所说的“新增的一个资源请求会导致死锁的发生”,一种算法是通过对不同类型资源总和的计算,比如我们有一类资源A,然后某个进程请求的资源类型A1,然后系统计算A1+目前在用的Ai,Aj等等,发现这个值已超出实际资源A的总和,则我们可以拒绝此请求。因为在死锁发生的情况时,是符合这个条件的。

死锁的检测


我们来看看最后一部分内容:死锁的检测。死锁的检测属于事前完全不做任何预防,避免操作,然后事中处理问题的一种解决方式。它采用的是一种周期性检测的一种方法。如果发现死锁了,则进行一些手段干预,来达到进程秩序恢复的效果,比如说我们可以撤销某些进程的资源请求,或者抢占资源的方式。不同的调整策略方式开销也会不同。

参考资源


[1].操作系统精髓与设计原理.第六章