死锁(Deadlock)是指在多进程/多线程环境中,两个或两个以上的进程/线程因竞争资源而陷入的一种相互等待的状态,若无外力干预,它们都将无法向前推进。简单来说,就是它们互相卡住了,谁也无法继续执行。
死锁产生的四个必要条件(必须同时满足)
互斥: 资源不能被共享,一次只能被一个进程/线程使用。
占有并等待: 进程/线程在等待新资源的同时,继续占有已分配的资源。
不可剥夺: 进程/线程已获得的资源,在未使用完之前,不能被强行剥夺,只能由该进程/线程主动释放。
循环等待: 存在一个进程/线程等待资源的循环链:P1 等待 P2 占有的资源,P2 等待 P3 占有的资源,…,Pn 等待 P1 占有的资源。
死锁避免策略(防止死锁发生)
避免死锁的核心思想是破坏产生死锁的四个必要条件中的一个或多个,或者在资源分配时进行谨慎决策(如银行家算法)。以下是主要的避免方法:
破坏“互斥”条件:
思路: 让资源可以被共享访问。
方法: 尽可能使用允许多个读者同时访问的只读资源;使用无锁数据结构(如原子操作、CAS);使用消息传递代替共享内存(如Go的Channel, Erlang的Actor模型)。
局限性: 很多资源本质上就是互斥的(如打印机、写文件、某些硬件寄存器),无法共享。
破坏“占有并等待”条件:
思路: 要求进程/线程在开始执行前一次性申请其所需的全部资源。
方法:
一次性分配: 进程在启动时或运行到某个点之前,必须申请并获得其整个生命周期可能需要的所有资源。如果无法全部满足,则等待。
或者, 进程在请求新资源时,必须先释放其当前持有的所有资源(然后再一次性重新申请所需资源)。
优点: 简单直接。
缺点:
资源利用率低: 资源可能在很长时间内被分配给一个进程却未被使用(饥饿其他进程)。
可能发生饥饿: 如果一个进程需要大量资源且其中某些资源长期被占用,该进程可能永远无法开始执行。
进程需要预先知道所需的所有资源,这通常难以预测。
破坏“不可剥夺”条件:
思路: 允许系统在必要时强行从进程/线程手中收回(剥夺)资源。
方法:
如果一个进程请求一个当前无法分配的资源,系统可以检查该资源是否被其他处于等待状态的进程持有。如果是,系统可以剥夺后者的资源分配给前者。
或者,当进程请求资源失败时,它必须释放所有当前持有的资源,以后再重新申请。
缺点:
实现复杂: 需要保存和恢复被剥夺资源的状态(如CPU寄存器的上下文),成本很高。
不适用于所有资源: 剥夺打印机上正在进行的打印任务、数据库事务中的中间状态等资源通常是不切实际或不可接受的,可能导致数据不一致或操作不完整。
破坏“循环等待”条件:
思路: 对系统中的所有资源类型进行全局排序,强制进程/线程按规定的顺序申请资源。
方法:
给每种资源类型分配一个唯一的序号(如1=扫描仪,2=打印机,3=磁盘)。
要求每个进程/线程在申请资源时,必须严格按照序号递增(或递减)的顺序提出申请。
例如:如果一个进程需要资源类型3(磁盘)和资源类型1(扫描仪),它必须先申请1(扫描仪),再申请3(磁盘)。它不能先申请3再申请1。
优点: 这是实际系统中最常用、最有效的预防死锁策略之一(尤其在数据库和操作系统中)。它破坏了循环等待链形成的可能性。
缺点:
资源申请顺序可能不符合进程的实际逻辑顺序,导致编程不便。
可能降低资源利用率(进程可能被迫提前申请暂时不用的资源)。
使用死锁避免算法:
思路: 在每次进行资源分配时,系统通过算法(如著名的银行家算法)动态检查分配后系统是否仍然处于安全状态(即存在一个序列<P1, P2, …, Pn>,使得按此序列分配资源后,每个进程都能顺利完成)。如果分配会导致不安全状态,则拒绝该次分配请求。
优点: 比预防策略允许更高的资源利用率和更灵活的申请。
缺点:
实现复杂: 算法本身计算开销较大。
需要预知最大需求: 进程需要事先声明其可能需要的最大资源量。
进程数量和资源种类通常是动态变化的,维护安全状态信息开销大。 在实际通用操作系统中较少直接使用,但在某些特定场景(如数据库事务管理)有应用。
死锁检测与恢复(允许死锁发生但能处理)
如果预防和避免策略的开销过大或难以实现,系统也可以选择允许死锁发生,但提供检测和恢复机制:
检测: 系统周期性地运行死锁检测算法(通常基于资源分配图),检查系统中是否存在死锁。
恢复: 一旦检测到死锁,系统采取措施打破死锁:
进程终止:
终止所有死锁进程: 简单粗暴,但代价可能很大。
逐个终止进程: 按照某种策略(如优先级、已运行时间、剩余计算量等)选择一个或多个“牺牲者”进程终止,释放其资源,直到死锁解除。被终止的进程稍后可能需要重启。
资源剥夺: 从一个或多个死锁进程强制剥夺某些资源,分配给其他进程,打破循环等待。需要处理好被剥夺资源的进程的状态回滚和重启问题。
总结
理解死锁的四个必要条件是解决死锁问题的关键。
死锁避免(预防) 侧重于设计系统时破坏必要条件。
破坏循环等待(资源排序法)是最常用且实用的策略。
破坏“占有并等待”和“不可剥夺”代价较高或有限制。
破坏“互斥”通常不可行。
死锁避免算法(如银行家算法) 理论性强,但实际应用受限。
死锁检测与恢复 适用于允许偶尔发生死锁并能承担恢复代价的系统。
在实际开发中(如多线程编程):
优先使用资源排序法: 为所有需要加锁的对象定义一个全局固定的顺序(如按内存地址排序),所有线程都严格按这个顺序获取锁。这是最有效的预防手段。
尽量减小锁的范围(临界区): 只在绝对必要的时候持有锁,尽快释放。减少持有锁的时间能降低死锁概率。
使用带超时的锁: 尝试获取锁时设置超时(如tryLock(timeout))。如果超时未获取到,放弃当前操作(可能释放已有锁重试或报错),避免无限等待。
避免嵌套锁: 尽量避免在一个锁保护的临界区内再去获取另一个锁。如果必须,务必使用资源排序法。
使用更高级的并发工具: 利用语言或框架提供的并发原语(如Java中的ConcurrentHashMap, Semaphore, CountDownLatch, CyclicBarrier;Go中的Channel),它们通常内部处理了同步问题,减少了直接使用锁的需要。
静态分析工具: 使用专门的工具检测代码中潜在的锁顺序问题。
理解死锁的原理和避免策略对于设计和开发健壮、可靠的并发系统至关重要。
















暂无评论内容