死锁的发生归根结底是因为对资源的竞争。因为大家都想要某种资源,但又不能随心所欲地得到所有资源,在争夺的僵局中,导致任何人无法继续推进。
如果有一组线程,每个线程都在等待一个事件的发生,而这个事件只能有该线程里面的另一线程发出,则称这组线程发生了死锁。这里的事件主要是资源的释放,在死锁状态中,没有线程可以执行、释放资源或被叫醒。
如果线程A和线程B交替执行,那么线程A和线程B均会因为无法获得对应的资源而无法继续执行也不能释放锁,从而造成死锁,如下图所示:
互斥条件:每个资源要么已经分配给了一个进程,要么就是可用的。
持有等待:即一个线程在请求新的资源时,其已经获得的资源并不释放,而是继续持有。
不可抢占:已经分配给一个进程的资源不能强制性地被抢占,即如果可以抢占一个资源,则也不会发生死锁。(凡是可以抢占的资源,均不会称为死锁的原因)。
循环等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。即如果你等我、我等你,大家都这样等着对方,就产生了死锁。
把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
算法总结如下:
利用抢占恢复;
利用回滚恢复;
通过杀死进程恢复。
破坏互斥条件:例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
破坏占有和等待条件:一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
破坏不可抢占条件;
破坏环路等待:给资源统一编号,进程只能按编号顺序来请求资源
图 a 的第二列Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
【检查一个状态是否安全的算法如下】:
如果一个状态不是安全的,需要拒绝进入这个状态。
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子(只有在他拿到两只筷子时才能进餐),并且一次只能拿起一根筷子。
经分析可知:放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用,为了实现对筷子的互斥访问,可以用一个信号量表示筷子,由这五个信号量构成信号量数组。
semaphore cpst[5] = {1, 1, 1, 1, 1};
while(true)
{
/*当哲学家饥饿时,总是先拿左边的筷子,再拿右边的筷子*/
p(cpst[i]);
p(cpst[(i+1)%5]);
eat();
/*当哲学家进餐完成后,总是先放下左边的筷子,再放下右边的筷子*/
v(cpst[i]);
v(cpst[(i+1)%5]);
}
假如五位哲学家同时饥饿而都拿起的左边的筷子,就会使五个信号量chopstick都为0,当他们试图去拿右手边的筷子时,都将无筷子而陷入无限期的等待。
至多只允许四个哲学家同时进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。定义信号量count,只允许4个哲学家同时进餐,这样就能保证至少有一个哲学家可以就餐。
semaphore cpst[5]={1,1,1,1,1};
semaphore count=4; // 设置一个count,最多有四个哲学家可以进来
void ph(int i)
{
while(true)
{
think();
P(count); //请求进入房间进餐 当count为0时 不能允许哲学家再进来了
P(cpst[i]); //请求左手边的筷子
P(cpst[(i+1)%5]); //请求右手边的筷子
eat();
V(cpst[i]); //释放左手边的筷子
V(cpst[(i+1)%5]); //释放右手边的筷子
V(count); //退出房间释放信号量
}
}
仅当哲学家的左右两支筷子都可用时,才允许他拿起筷子进餐。利用信号量的保护机制实现的思想是通过记录型信号量mutex对取左侧和右侧筷子的操作进行保护,使之成为一个原子操作,这样可以防止死锁的出现。
semaphore cpst[5]={1,1,1,1,1};
semaphore mutex=1; // 这个过程需要判断两根筷子是否可用,并保护起来
void ph(int i)
{
while(true)
{
think();
P(mutex); // 保护信号量
P(cpst[i]); // 请求左手边的筷子
P(cpst[(i+1)%5]); // 请求右手边的筷子
V(mutex);
eat();
V(cpst[i]); // 释放左手边的筷子
V(cpst[(i+1)%5]); // 释放右手边的筷子
}
}
规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则先拿起他右边的筷子,然后再去拿他左边的筷子。按此规定,将是1、2号哲学家竞争1号筷子,3、4号哲学家竞争3号筷子。即五个哲学家都竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一个哲学家能获得两支筷子而进餐。
semaphore cpst[5]={1,1,1,1,1};
void ph(int i)
{
while(true)
{
think();
if(i%2 == 0) //偶数哲学家,先右后左。
{
P(cpst[(i + 1)%5]) ;
P(cpst[i]) ;
eat();
V(cpst[(i + 1)%5]) ;
V(cpst[i]) ;
}
else //奇数哲学家,先左后右。
{
P(cpst[i]) ;
P(cpst[(i + 1)%5]) ;
eat();
V(cpst[i]) ;
V(cpst[(i + 1)%5]) ;
}
}
}
参考:https://blog.csdn.net/fuziwang/article/details/79809994
https://www.cnblogs.com/edisonchou/p/5061886.html