死锁的四个必要条件以及如何避免死锁

    xiaoxiao2024-12-22  3

    死锁 是多个并发进程因为争夺系统资源产生相互等待的现象

    原理:当一个进程集合中的每一个进程都在等待 只能由该进程集合中的其他进程才能引发的事件,那该进程集合发生了死锁

    打个比方,一个女孩子手里有个洋娃娃,一个男孩子手里有个小汽车,女孩子想玩汽车,但又不想把洋娃娃让给男孩子,男孩子想玩洋娃娃,但也不想把汽车给女孩子玩,他们两个就这样一直僵持下去,就会造成死锁,洋娃娃和汽车就相当于是一个不可强占资源,男女孩子相当于两个进程.

    造成死锁的原因
    系统资源有限进程的推进顺序不恰当

    死锁产生的4个必要条件

    互斥条件

    请求与保持/占有与等待条件

    不可剥夺条件

    环路等待/循环等待条件

    互斥条件是资源每次只能被一个执行流使用

    请求与保持条件,一个进程自身占有资源,同时还有资源未得到满足,请求等待其它进程释放资源

    不可剥夺(抢占)条件,别的进程已经占有某个资源,不能因为自己也需要那个资源就把它强占过来.只有等别的进程释放后,才可以抢占.

    循环等待(环路等待)条件,有两个或两个以上的进程组成一条环路,该环路中的每个进程都在等待着下一个进程所释放的资源

    以上四个条件均满足时必然会造成死锁,发生死锁的进程无法执行下去,它们所持有的资源也无法释放,这会导致CPU吞吐量下降,影响了计算机性能且浪费资源.也可能直接导致系统卡死.

    避免死锁

    破坏四个必要条件之一

    加锁顺序一致

    避免锁未释放的场景

    死锁预防

    破坏互斥条件

    由于互斥条件可以保证线程的安全,所以一般不会破坏这个条件,否则会造成线程不安全的情况

    破坏请求与保持条件

    方法一:资源一次性分配,进程在运行过程中,申请运行过程中的所需要的全部资源,这种方法简单,安全,但是可能会导致资源的浪费或者降低资源的利用率,也会造成进程的饥饿

    方法二:允许进程只获得运行初期需要的资源,就可以开始运行了,在运行的过程中逐步释放已经使用完的资源,然后再去请求新的资源,比起第一种方法,资源得到有效的利用,也尽可能的避免饥饿问题

    破坏不可抢占条件

    当一个进程在请求资源时没有得到满足,先暂时释放当前所占有的资源,待以后需要的时候再去请求,这就意味着已经占有的资源被强占了,这种方法实现起来困难,且可能会导致进程之前做的工作失效,而且反复的申请释放,不仅延长了进程的周转时间,还降低了系统的吞吐量

    破坏环路等待

    可以将每个资源编号,来定义线程访问的规则,比如说一个线程只能申请比占有资源编号大的资源

    也就是说,进程 C 无法申请资源 1,因为它占有了资源 3 ,而资源 3 大于资源 1,所以无法申请.这样就不会造成环路等待

    比较低效,资源的执行速度会变慢,同时也降低了资源的利用率,例,如果资源 1 是可用的,我们定义的是线程 3 无法访问资源 1 ,这样就降低了资源利用率.

    条件处理方式互斥一切都使用假脱机技术占有和等待在开始就请求全部资源不可抢占抢占资源环路等待对资源按序编号

    从死锁中恢复

    如果已经检测到死锁则如何恢复?

    抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态

    终止进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态

    终止所有的死锁进程。这种方式简单粗暴,但是代价很大,很有可能会导致一些已经运行了很久的进程前功尽弃

    逐个终止进程,直至死锁状态解除。该方法的代价也很大,因为每终止一个进程就需要使用死锁检测来检测系统当前是否处于死锁状态

    每次都应该采用最优策略来选择终止一个“代价最小”的进程来解除死锁状态


    死锁避免

    在使用前进行判断,只允许不会产生死锁的进程申请资源,比较典型的算法是银行家算法.

    如果一个进程的请求会导致死锁,则不启动该进程,如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请

    银行家算法

    对每一个请求进行检查,如果满足这个请求会达到安全状态则满足,否则就推迟这一请求的满足;为了检测状态安全,银行家必须要考虑他自己是否有足够的资源满足客户,如果可以那么这笔账就是可以收回的,如果所有投资都可以被收回,那么状态就是安全的

    假定银行共有 12 万需要借贷,现在有借贷人(进程) P1,P2,P3 需要借贷,在 T 时刻状态如下图

    银行在 T 时刻剩余 3 万需要贷出,如果直接 P1 请求贷给它,那么银行肯定会拒绝该请求,因为贷给 P1 这笔账无法收回,给P3 也无法收回,那么先借贷给 P2 ,这样 P2 满足需求后会返回4万,然后在借贷给 P3 ,最后再借给 P1

    所以我们会发现并不是不安全状态一定产生死锁,而是不安全状态容易产生死锁

    银行家算法通过 拒绝可能引起不安全状态的请求来避免死锁


    另外从代码设计的角度来规避死锁问题

    短:让临界区的代码尽量短平:临界区代码尽量不调用复杂函数快:让临界区代码执行速度尽量快

    其它种类的锁

    两阶段加锁

    在数据库系统中,一个经常发生的操作就是:锁住一些记录,然后更新所有锁住的记录,有乐观锁,悲观锁等.

    参考文章:https://blog.csdn.net/qq_43763344/article/details/1027

    通信锁

    当在网络上,两个进程或多个进程利用发消息进行通信时,进程A向进程B发生请求消息,然后阻塞直到B回复,假设这个信息丢失,那么A将一直阻塞等待回复,而B会阻塞等待 一个向其发送命令的请求;简要来说就是:A发送了消息,一天什么也不做,就在手机旁边等B给他回复消息,B啥也不做,一天也守在手机旁等待A给他发消息,可熟不知如果A发出去的消息丢失,A B就都会阻塞等待对方

    通过 超时 的方法来中断通信死锁。大概就是:消息发送出去以后,会返回一个预期的回复时间,如果过了这个时间还没有回复的话,则可以判断消息丢失不会再回复了,则A 再重新发送;这种方法同样适用于资源死锁

    读写锁

    读写锁的适用的场景是,少量写临界资源的线程和大量读临界资源的线程

    读写锁有更好的并行性,有三种状态

    读模式下的加锁状态写模式下的加锁状态不加锁状态

    加锁规则:写的线程是互斥的,读的线程是共享的

    不能同时写,但是可以同时读一个执行流进行写的时候,其他执行流不可读也不可写一个执行流读的时候,其他执行流不可写,但可以读

    一次只有一个线程占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁.

    下面这段特别重要 当多个线程以读模式获取读写锁的时候,这个时候有一个线程想要以写模式获取读写锁,这个时候底层会阻塞该线程,直到所有读模式的线程释放锁才可以去写,但是如果一个线程想要以写模式获取锁,但是后面也有读模式的线程想要获取读写锁,这时候会阻塞后面读模式的线程.保证以写模式的线程不会等待太长的时间,也避免锁资源被长时间占用.

    接口 pthread_rwlock_t rw pthread_rwlock_init(&rw,NULL) pthread_rwlock_destroy(&rw)

    加锁: 读pthread_rwlock_rdlock(&rw) 写pthread_rwlock_wrlock(&rw) 解锁: pthread_rwlock_unlock(&rw)

    另外对于多个线程去占有读模式下的读写锁,里面也是有引用计数的,释放一次引用计数减一,加锁一次引用计数加一

    参考文章:读写锁和互斥锁的区别?

    自旋锁

    啥是自旋锁?

    自旋锁和互斥锁特别相似,他们都保证了临界资源的唯一访问性,但是互斥锁如果一旦被上锁,其它的线程会陷入阻塞状态,而自旋锁如果被一个线程获得后,其它线程不会陷入阻塞状态,它在一定的时间段内会一直判断自旋锁是否已经释放,所以会大量的占用 CPU 资源.

    自旋锁适合用于锁保持时间非常短的情况,它的效率要比互斥锁高,如果锁保持时间比较长,则使用互斥锁.

    活锁

    活锁分为单一实体和协同导致的活锁

    单一实体的活锁:例如,线程从队列中拿出一个任务来执行,如果任务执行失败,那么将任务重新加入队列,继续执行。假设任务总是执行失败,或者某种依赖的条件总是不满足,那么线程一直在繁忙却没有任何结果

    协同导致的活锁:

    生活中的例子,两个人在窄路相遇,同时向一个方向避让,然后又向另一个方向避让,如此反复…

    通信中也有类似的例子,多个用户共享信道(最简单的例子是大家都用对讲机),同一时刻只能有一方发送信息。发送信号的用户会进行冲突检测, 如果发生冲突,就选择避让,然后再发送。 假设避让算法不合理,就导致每次发送,都冲突,避让后再发送,还是冲突

    计算机中的例子:两个线程发生了某些条件的碰撞后重新执行,那么如果再次尝试后依然发生了碰撞,长此下去就有可能发生活锁

    解决方法:解决协同活锁的一种方案是调整重试机制

    比如引入一些随机性,如果检测到冲突,那么就暂停随机的一定时间进行重试,这会大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制

    另外为了避免可能的死锁,适当加入一定的重试次数也是有效的解决办法。尽管这在业务上会引起一些复杂的逻辑处理,比如约定重试机制避免再次冲突。 例如自动驾驶的防碰撞系统(假想的例子),可以根据序列号约定检测到相撞风险时,序列号小的飞机朝上飞, 序列号大的飞机朝下飞

    活锁和饥饿的区别

    活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败…

    假设事务 T2 在不断的重复尝试获取锁 R,那么这个就是活锁

    活锁可以认为是一种特殊的饥饿。

    如果事务T1 封锁了数据R , 事务T2又请求封锁R,于是T2等待,T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待,然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求…T2可能永远等待,这个是饥饿现象

    活锁应该是一系列进程在轮询地等待某个不可能为真的条件为真,活锁的时候进程是不会阻塞,而是处于繁忙式等待,不断的获取CPU资源,所以导致耗尽 CPU,但是对于饥饿,如果一个进程获取不到锁,就会进入阻塞状态,这对 CPU 的来说是高效的.

    最新回复(0)