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

Posted The Gao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统关于多线程同步中的死锁问题一篇文章让你彻底搞明白死锁到底是什么情况及如何解决死锁相关的知识,希望对你有一定的参考价值。

死锁的引出案例

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

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系统中,对于死锁的处理都是采用死锁忽略这一方法。

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

以上是关于操作系统关于多线程同步中的死锁问题一篇文章让你彻底搞明白死锁到底是什么情况及如何解决死锁的主要内容,如果未能解决你的问题,请参考以下文章

进程互斥同步及通信死锁问题操作系统

五个案例让你明白GCD死锁

多线程之死锁定位及故障分析,尽可能避免线上事故(十三)

ios多线程同步异步、串行并行队列、死锁

4-5 《Java中多线程重点》——继承Thread实现Runnable死锁线程池Lambda表达式

Java多线程产生死锁的一个简单案例