【操作系统】关于多线程同步中的死锁问题【一篇文章让你彻底搞明白死锁到底是什么情况及如何解决死锁】

死锁的引出案例

以操作系统课程中著名的“生产者-消费者”案例为例,解释一下什么是死锁。

void producer(){//生产者函数
    while(1){
        P(empty);//判断缓冲区空槽是否>=0,并将空槽-1
        P(mutex);//进入临界区并上锁
        pfunc();//执行生产函数,产生一个数据放置在占据的空槽上
        V(mutex);//离开临界区并解锁
        V(full);//将满槽+1,并判断满槽是否<=0
    }
}
void consumer(){//消费者函数
    while(1){
        P(full);//判断缓冲区满槽是否>=0,并将满槽-1
        P(mutex);//进入临界区并上锁
        cfunc();//执行消费函数,取出满槽上的数据
        V(mutex);//离开临界区并解锁
        V(empty);//将空槽+1,并判断空槽是否<=0
    }
}

初始化时,设置mutex=1,即同时只能有一个线程访问临界区;设置full=0,empty=maxsize,即初始时缓冲区没有数据,满槽数量为0,空槽数量为缓冲区最大值。

此时若执行消费者线程,且操作系统分配的时间片刚好满足执行一次线程的执行序列,消费者线程首先对缓冲区执行P操作,判断满槽是否>=0,并将满槽-1。满槽在初始化时置为0,因此full-1后值为0,是<0的,消费者线程阻塞,进入等待队列。

接下来调度执行一次生产者线程的执行序列,生产者线程首先对缓冲区执行P操作,判断空槽是否>=0,并将空槽-1。空槽在初始化时置为maxsize,因此empty–后一定是>=0的,继续向后执行P(mutex),即生产者线程进入临界区并上锁。通过执行生产函数pfunc(),产生数据放在缓冲区。再执行V(mutex),将临界区解锁。最后执行V(full)时,将缓冲区满槽数量+1,此时full++后的值为0,是<=0的,所以等待队列一定有线程在阻塞,因此唤醒阻塞的消费者线程。

这是一次完整的“生产者-消费者”案例的执行逻辑,也说明了多线程同步中对于临界区的保护。那么死锁是如何出现的呢?大家看下面的程序。

void producer(){//生产者函数
    while(1){
        P(mutex);
        P(empty);
        pfunc();
        V(mutex);
        V(full);
    }
}
void consumer(){//消费者函数
    while(1){
        P(mutex);
        P(full);
        cfunc();
        V(mutex);
        V(empty);
    }
}

依然是基于上述“生产者-消费者”案例的background,在生产者和消费者函数调整了两个P操作的先后顺序。此时,我们在分析,如果按照上述的执行流程,会出现什么情况呢?

首先执行消费者的序列,将mutex-1,再将full-1,此时full<0,说明缓冲区中原本没有数据,消费者序列阻塞,进入等待序列。然而此时mutex依然是0,导致生产者线程无法访问临界区,也就无法生产数据,而消费者线程又需要生产者线程执行来放入数据。因此,此时就会陷入了一种“你等我,我等你”的死循环局面,这就是死锁。

死锁的概念

将上述进行归纳总结,就可以得到死锁的概念。

在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。

那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。

死锁的发生条件

互斥条件

互斥条件是指多个线程不能同时使用同一个资源。如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。
在这里插入图片描述

持有并等待条件

持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
在这里插入图片描述

不可剥夺条件

不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
在这里插入图片描述

环路等待条件

环路等待条件是指,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图。
在这里插入图片描述

死锁的处理

死锁的处理逻辑也非常好想,因为上述四个条件都是死锁出现的必要条件,缺一不可,那么我们在设计程序时,只要破坏其中一项条件就可以不让死锁发生。

(1)一次性申请所有资源,此时就不会再占有一个资源时再去申请其他资源,即解决了死锁条件中的持有并等待条件这一条件。

不过这样的解决方法会出现两个缺点:①需要宏观设计,即要想到所有需要的资源,造成编程困难;②许多资源要很久后才会用到,造成资源浪费。

(2)对资源按照类型进行排序,资源申请必须按序进行,即解决了死锁条件中的环路等待条件这一条件。

这样也会导致资源浪费的情况。

(3)死锁检测。通过“银行家算法”,对于每一次的请求判断是否会出现死锁,从而产生一个能够不产生死锁的安全序列。这样会导致,程序的时间复杂度T=O(m*n^2)较高。

若通过改进的“银行家算法”,即不是每一次的请求都要判断,而是当出现了死锁之后,进行回滚,回滚至死锁出现前的情况。这样又会导致,比如现在进程是将文件写入磁盘,如果文件已经写入了,难道还要将文件再回滚到没写入之前的情况嘛。

(4)死锁忽略。因为死锁这一情况本身的出现概念是极低的,而且对于一般的个人PC机来说,一次重启即可以轻松解决,因此在Windows和Linux系统中,对于死锁的处理都是采用死锁忽略这一方法。

其实,现实情况是,基本上只有在航天或银行的操作系统中,才需要考虑死锁这一情况。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

The Gao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值