傍晚下班高峰,城市中心一个没有红绿灯的十字路口,四辆车同时抵达。北面来的车想直行到南边,却被东面来的车挡住了去路;东面的车要左转,却被南面的车挡住;南面的车想右转,又被西面的车挡住;西面的车刚要直行,北面的车又堵在正前方。四辆车谁也不肯后退,喇叭声此起彼伏,整个路口陷入奇妙的僵局——这就是死锁的活写照。 在计算机世界里,进程之间争抢 CPU、内存、打印机这类“资源”时,也会发生一模一样的僵局:每个进程都握着部分资源不放,同时等待其他进程释放资源,结果大家都卡在原地,系统像被按下了暂停键。这篇文章带你认识死锁的定义与四个必要条件,学会用“资源分配图”这个侦察工具揪出潜在的环路,再逐一了解破坏四个条件的预防策略,最后算一笔“安全与效率”的经济账——就像为路口加装红绿灯,必然会牺牲一点“随心所欲”的通行效率。

岗位关联

对 AI 从业者而言,死锁与预防是并发系统稳定性的地基:分布式训练中的显存竞争、推理服务的多线程锁、特征工程里的数据库连接池,全都绕不开它,也是系统设计与面试的高频考点。

目录
  1. 可视化演示
  2. 死锁基本概念
  3. 四个必要条件
  4. 资源分配图
  5. 破坏互斥条件
  6. 破坏占有等待
  7. 破坏不可剥夺
  8. 破坏循环等待
  9. 权衡与工程决策
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过Coffman四必要条件、资源分配图循环检测和预防策略对比三个模式,直观展示死锁成因与消除机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Coffman死锁四必要条件 互斥:资源同一时刻只能被一个进程占用 循环等待:进程间形成资源请求环 互斥 ∧ 占有等待 ∧ 不剥夺 ∧ 循环等待 四条件同时成立才会发生死锁

死锁基本概念

傍晚的十字路口,四辆车各占一角。北车要直行,被东车挡住;东车要左转,被南车挡住;南车要右转,被西车挡住;西车要直行,又被北车挡住。四辆车都握着自己的位置不放,又都等着别人先动,喇叭声吵成一片,却没有一辆车能挪动半步。这个场景,就是操作系统教材里“死锁”一词最形象的注脚。

把车换成进程,把车道换成资源——内存、打印机、显存、文件锁——一切立刻对得上号。进程 A 持有打印机,想用扫描仪,而进程 B 持有扫描仪,正等着打印机释放。两个进程都“占着位置不干活”,互相观望,谁也无法继续执行。死锁不是程序报错,不是崩溃,而是一种安静而顽固的停滞。

严谨地说,死锁是指一组进程中的每一个进程,都在等待一个“仅由该组内其他进程才能产生”的事件(通常是释放资源)。参与死锁的进程全部处于阻塞状态,资源被白白占用,系统吞吐量降为零。它不消耗 CPU,却让整个系统形同瘫痪,因此是并发系统设计中最隐蔽的敌人之一。

正如四辆车里的任何一辆都无法指望路口外的行人来指挥交通,死锁中的每个进程也只能干等组内“队友”——而队友同样在等它。理解了这种互相成全的停滞,我们才能继续追问:什么样的条件凑在一起,才会酿造出这样的僵局?

死锁  ⟺  ∀pi∈S, wait(pi) 仅能由 S 中其他进程 pj (j≠i) 的推进来解除\displaystyle \text{死锁} \iff \forall p_i \in S,\ \text{wait}(p_i) \text{ 仅能由 } S \text{ 中其他进程 } p_j\ (j \neq i) \text{ 的推进来解除}
这个公式把“僵局”翻译成了数学语言:每个进程的等待,只有组内其他进程能解开。
  1. 进程集合 S死锁考察的不是单个进程,而是一组互相牵扯的进程,就像十字路口上四辆车必须整体分析。
    S={p1,p2,…,pn}S = \{p_1, p_2, \dots, p_n\}
  2. 等待事件每个进程都在等一个外部事件,最常见的就是“别人释放资源”这个事件。
    wait(pi):进程 pi 等待资源释放\text{wait}(p_i): \text{进程 } p_i \text{ 等待资源释放}
  3. 组内互等能解除等待的只有同组的其他进程,外部进程无法介入,这是僵局的核心。
    ∀pi∈S, ∃pj∈S, j≠i:pi 等 pj\forall p_i \in S,\ \exists p_j \in S,\ j \neq i: p_i \text{ 等 } p_j
  4. 双向蕴含互推关系表示两边互为充要条件,定义既精确又完整,可以用来严格判定死锁。
    死锁  ⟺  以上等待关系成立\text{死锁} \iff \text{以上等待关系成立}

四个必要条件

仔细盯着那个十字路口看,你会发现四辆车能僵住,其实缺一不可。首先,路口同一时刻只能过一辆车(互斥);其次,每辆车都占着当前车道、还想要前面的路段(占有并等待);再次,没有车愿意主动倒车,交警也没来(不可剥夺);最后,四辆车刚好围成一个“你等我、我等它”的圈(循环等待)。

计算机科学家把这几件事总结为死锁的四个必要条件:互斥条件、占有并等待、不可剥夺、循环等待。四者必须同时成立,死锁才会发生;反过来,只要破坏其中任何一个,死锁就不可能形成——这正是后面所有预防策略的理论总纲。

用逻辑语言看,四个条件合起来是死锁的充分条件,而每个条件单独看都是死锁的必要条件。这意味着:系统没有死锁,不代表四个条件都不满足;但系统一旦死锁,四个条件必然全部在场。

四辆车要想脱困,要么拓宽路口、要么提前约定通行顺序、要么请交警强制指挥——对应到系统里,就是下一节起我们要逐一拆解的四大预防策略。记住这张“四条件清单”,后面的故事全是围绕它展开的。

\displaystyle \text{有死锁} \Rightarrow M \wedge B \wedge N \wedge C,\quadM=\text{互斥},\ B=\text{占有并等待},\ N=\text{不可剥夺},\ C=\text{循环等待}
这个公式是一张“死锁配方表”:四种原料缺一不可,删掉任何一种,死锁这道菜就做不出来。
  1. 互斥条件 M同一时刻资源只能被一个进程使用,像路口同一时间只能过一辆车。
    M:同一时刻资源仅限一个进程使用M: \text{同一时刻资源仅限一个进程使用}
  2. 占有并等待 B进程占着已有资源不放手,同时又申请新资源,像车占着车道还想要前方路段。
    B:held(p)≠∅∧want(p)≠∅B: \text{held}(p) \neq \varnothing \land \text{want}(p) \neq \varnothing
  3. 不可剥夺 N资源只能由持有者主动释放,系统不能强行收走,像谁都不肯主动倒车。
    N:¬(系统可强行回收)N: \neg(\text{系统可强行回收})
  4. 循环等待 C进程之间形成一环扣一环的等待链,像四辆车首尾相接围成一圈。
    C:p1→p2→⋯→pk→p1C: p_1 \to p_2 \to \cdots \to p_k \to p_1
  5. 合取关系四个条件用“与”连接,任何一个条件缺席,死锁都不成立。
    M∧B∧N∧C⇒死锁M \land B \land N \land C \Rightarrow \text{死锁}

资源分配图

要在十字路口判断“会不会堵死”,最直观的办法是把每辆车和每条车道画成一张关系图:车要用的车道,画一条指向车道的线;车道正被某辆车占着,就画一条从车道指向车的线。这张图一摆出来,谁在等谁、谁会等成环,一目了然。

操作系统里这叫资源分配图(Resource Allocation Graph):圆圈表示进程,方框(内部加点表示实例数量)表示资源类型;进程指向资源的边叫请求边,资源指向进程的边叫分配边。图上的每一条边,都是一句“我想要你”或“它属于我”的声明。

判定规则非常漂亮:若图中无环,系统一定不会死锁;若图中有环,且每个资源都是单实例(路上只有一条车道),则系统必然死锁。多实例资源时,环只是死锁的必要条件——环里可能还有剩余实例“救场”。

回到十字路口:画好这张图,你一眼就能看到一条“A→车道→B→车道→C→车道→D→车道→A”的闭环。发现环之后怎么办?交警上场,开始拆条件——这正是下一节要讲的重点。

\displaystyle G = (P \cup R, E),\quad E = E_{req} \cup E_{alloc},\quadE_{req} = \{(p_i, r_j)\},\quad E_{alloc} = \{(r_j, p_i)\}
这个公式给出了画图的“语法”:两类节点、两类边,图一画好,死锁的环路就无处藏身。
  1. 两类节点图中只出现进程与资源两类元素,分别画成圆圈和方框。
    V=P∪RV = P \cup R
  2. 两类边请求边表示“我想用”,分配边表示“我正用着”,方向不同,含义相反。
    E=Ereq∪EallocE = E_{req} \cup E_{alloc}
  3. 环路检测沿着有向边走能回到起点,就找到了一条环,也就是循环等待的图影。
    环: p1→r1→p2→r2→⋯→p1\text{环: } p_1 \to r_1 \to p_2 \to r_2 \to \cdots \to p_1
  4. 判定结论单实例资源下环等价于死锁;多实例资源下,还要看环内资源是否有剩余实例。
    单实例且存在环  ⟺  死锁\text{单实例且存在环} \iff \text{死锁}

破坏互斥条件

要让四辆车不再互相堵死,最“釜底抽薪”的思路是:把路口修成立交桥,所有车各走各的道,谁也不用等谁。对应到计算机里,就是破坏互斥条件——让资源可以被多个进程同时使用。

遗憾的是,互斥是许多资源与生俱来的属性:打印机不可能同时打印两份文档,磁带机不可能同时写两个任务,显存缓冲区也不方便被两个内核同时写入。强行取消互斥,要么违反物理现实,要么引入新的数据混乱。因此,“破坏互斥”在四种策略里是适用面最窄的一种。

真正的工程解法是“曲线救国”——假脱机(SPOOLing)技术。以打印机为例,系统先让各进程把输出写到磁盘上的打印队列,由专门的假脱机进程统一排队、独占打印。进程看来打印机变成了“共享设备”,互斥被悄悄绕开,死锁自然无从谈起。代价是额外的磁盘空间与延迟。

立交桥造价高昂、不是每个路口都修得起。破坏互斥条件也一样:只对少数天然可共享的资源有效,应用场景局限。既然这条路常常走不通,交警把目光转向第二招——从源头上禁止“边占边等”。

¬M⇒¬(死锁)\displaystyle \neg M \Rightarrow \neg(\text{死锁})
这个公式说:只要互斥条件不成立,死锁就必然不会发生——很诱人,可惜能天然共享的资源太少了。
  1. 否定互斥让资源可以被多个进程同时访问,从根本上消除“一个萝卜一个坑”的争抢。
    ¬M:资源可同时被多个进程访问\neg M: \text{资源可同时被多个进程访问}
  2. 蕴含翻译死锁的必要条件被拆掉一根支柱,整座建筑随之倒塌。
    ¬M⇒¬(死锁)\neg M \Rightarrow \neg(\text{死锁})
  3. 现实约束只有天然可共享的资源才能直接改造;独占设备要靠假脱机缓冲间接实现共享。
    r∈Rshared∨r→假脱机缓冲r \in R_{\text{shared}} \lor r \to \text{假脱机缓冲}

破坏占有等待

四辆车之所以形成僵局,是因为每辆车都“先占了一条道,再算计前面的路”。如果换个规则:谁想上路,必须一次性把整条路线的通行权全部申请到手,否则就不准上路,那么要么四辆车畅通无阻依次放行,要么谁也没资格进场——僵局自然不存在。

这就是“破坏占有并等待”策略,也称资源预分配:进程运行前一次性申请它需要的全部资源,系统要么全部分配,要么一个都不分配。进程在执行期间不再提出新的资源请求,也就断掉了“占着等”的中间状态。

代价同样明显:资源利用率大幅下降——进程也许只用到打印机十分钟,却要独占它整个生命周期;更糟的是饥饿问题,低优先级进程可能永远等不齐它要的资源,出现“饿死”现象。因此这种策略只适合任务时间短、资源需求明确的场景(例如数据库事务的预锁)。

一次性分配就像要求四辆车出发前先摇到“整条路线的同时通行权”,安全归安全,却让本来就紧张的车道大量闲置。交警很快发现,还有更灵巧的第三招——允许强制倒车。

运行期间:  held(p)=∅∨want(p)=∅\displaystyle \text{运行期间: }\ \text{held}(p) = \varnothing \lor \text{want}(p) = \varnothing
这个公式是“不许边占边等”的数学表达:要么手里没货,要么不在等货,两者必居其一。
  1. 持有与需求分别记录进程已占资源与正在等待的资源,作为约束的对象。
    held(p), want(p)\text{held}(p),\ \text{want}(p)
  2. 析取约束任意时刻两者不能同时非空,“占有并等待”的中间态被彻底排除。
    held(p)=∅∨want(p)=∅\text{held}(p) = \varnothing \lor \text{want}(p) = \varnothing
  3. 预分配保障所有资源在执行前一次性分配到位,执行期间不再产生新的申请。
    运行前: alloc(p)=需求全集\text{运行前: } \text{alloc}(p) = \text{需求全集}
  4. 代价投射长期独占资源导致利用率下降,低优先级进程可能永远等不齐资源。
    U↓, 饥饿风险↑U \downarrow,\ \text{饥饿风险} \uparrow

破坏不可剥夺

现实生活里,死堵的路口往往靠交警一声令下:“那辆车,往后倒!”虽然司机不情愿,但倒车之后,僵局立刻松动。操作系统里同样可以引入这位“交警”——剥夺机制。

破坏不可剥夺条件的基本思想是:当一个进程申请不到新资源时,系统可以强行回收它已占有的部分资源,重新分配给等待中的进程。持有者被剥夺资源后,必须回滚到安全状态,等将来再重新申请、重新执行。

这个策略对“状态可保存、可恢复”的资源非常有效。CPU 的寄存器、内存页表都是好例子:中断一下,保存现场,资源就能转给别人。但打印机不行——打了一半的文档没法“回滚”重来。所以剥夺策略的应用范围,取决于资源的可回滚程度。

交警的强制倒车不是万能的——倒出去的是一辆车,可它刚走过的路径如果无法还原(文档打了半页),这条路就行不通。于是,最优雅也最常用的第四招登场了:从规则上规定谁先走、谁后走。

held(pi)←∅,state(pi)←rollback(state(pi))\displaystyle \text{held}(p_i) \leftarrow \varnothing,\quad \text{state}(p_i) \leftarrow \text{rollback}(\text{state}(p_i))
这个公式写的就是“交警倒车令”:先清空进程手里的资源,再把它送回被剥夺前的安全位置。
  1. 强制回收系统把进程已占资源全部清空,如同交警命令倒车腾出车道。
    held(pi)←∅\text{held}(p_i) \leftarrow \varnothing
  2. 状态回滚进程无法从当前进度继续,只能回到被剥夺前的安全现场,重新开始。
    state(pi)←rollback(state(pi))\text{state}(p_i) \leftarrow \text{rollback}(\text{state}(p_i))
  3. 可回滚前提只有状态可完整保存与恢复的资源,才经得起这种“倒车再出发”。
    可保存/可恢复⇒可剥夺\text{可保存/可恢复} \Rightarrow \text{可剥夺}

破坏循环等待

最后也是工程上最实用的思路:不碰车、不抢道,只改一条规则——给所有路口编上号,任何车只能往编号更大的路口开,禁止“回头”申请。这样无论怎么开,都不可能绕成一圈:因为路线的编号严格递增,根本没有回到起点的可能。

这就是破坏循环等待的资源编号法:系统为每种资源分配一个唯一的全局编号,规定所有进程必须按照编号递增的顺序申请资源。以后要想申请编号较小的资源,必须先释放手中编号较大的资源。环路需要的“反向申请”被规则彻底禁止,从结构上杜绝了死锁。

编号法实现简单、开销低,是实际操作系统的首选预防策略。但设计者必须谨慎设定编号:编号顺序会影响资源使用的灵活性,而且新增资源类型的编号划定、对已有代码的申请顺序改造,都需要在系统设计初期就定好规矩,后期很难更改。

让所有车都往“大号路口”开,听起来有点绕路,却用最轻的代价拆掉了那个致命闭环。四种预防策略讲完,最后摆在我们面前的问题是:这些“安全措施”本身,要花多少钱?

f:R→N,∀p: f(ra1)<f(ra2)<⋯<f(rak)\displaystyle f: R \to \mathbb{N},\quad \forall p:\ f(r_{a_1}) < f(r_{a_2}) < \cdots < f(r_{a_k})
这个公式是“递增通行证”规则:进程手里的资源编号,必须一路走高,永远不回头。
  1. 全局编号为每类资源分配一个互不相同的自然数编号,像给每个路口挂一块唯一门牌。
    f:R→Nf: R \to \mathbb{N}
  2. 递增申请进程每一次新申请,目标资源的编号必须比已持有资源更大。
    f(ra1)<f(ra2)<⋯<f(rak)f(r_{a_1}) < f(r_{a_2}) < \cdots < f(r_{a_k})
  3. 禁止反向环路中必有一条编号下降的“回头”边,而这条边被规则堵死。
    ¬∃:f(rai)>f(rai+1)\neg \exists: f(r_{a_i}) > f(r_{a_{i+1}})
  4. 工程启示编号一经发布便难以修改,系统设计初期就要完成全局资源编号规划。
    编号顺序→系统的长期约束\text{编号顺序} \to \text{系统的长期约束}

权衡与工程决策

红绿灯能防止路口死锁,但代价是车流随到随走的自由;立交桥也是同样的道理。操作系统里的每一种死锁预防策略,本质都是用资源利用率和系统吞吐量去交换“绝对安全”。理解了这笔账,才谈得上工程选型。

一次性预分配让资源长期闲置,利用率跌得最惨;剥夺和回滚让系统忙于“存档读档”,额外开销巨大;编号法则在灵活性和安全性之间找到了较好的平衡点,因此最常被实战采用。破坏互斥的应用面最窄,通常只在假脱机这类特定场景出现。

衡量这笔交易,可以看两个指标:资源利用率 U(资源真正干活的时间占比)和系统吞吐量 Θ(单位时间完成的进程数)。预防策略越激进,U 和 Θ 往往越低;但死锁一旦发生,U 和 Θ 会直接归零。工程决策的实质,就是在“预防成本”与“死锁损失”之间找最小代价。

回到最初的十字路口:如果没有交警、没有红绿灯,四辆车只能靠“谁都不让”的默契堵死在那里;而有了合理规则,哪怕规则让人多等几秒、多绕几步,整条街反而能川流不息。操作系统的死锁预防,说到底也是这个道理——用一点点效率,换整个系统永不停摆的稳定。

U=TuseTtotal,Θ=N完成Δt\displaystyle U = \frac{T_{use}}{T_{total}},\qquad \Theta = \frac{N_{\text{完成}}}{\Delta t}
这个公式给出了“这笔账怎么算”:利用率和吞吐量就是安全策略的两本明细账。
  1. 资源利用率资源实际使用时间占总时间的比例,预分配会让分母变大、利用率下降。
    U=Tuse/TtotalU = T_{use} / T_{total}
  2. 系统吞吐量单位时间内完成的进程数量,回滚与等待都会拖累这个数字。
    Θ=N完成/Δt\Theta = N_{\text{完成}} / \Delta t
  3. 权衡关系利用率和吞吐量越高,资源竞争越激烈,死锁风险也随之上升。
    U, Θ↑⇒死锁风险↑U,\ \Theta \uparrow \Rightarrow \text{死锁风险} \uparrow
  4. 实践倾向综合成本与可控性,资源编号递增申请是工业界最常用的预防手段。
    预防选型: 递增编号申请\text{预防选型: 递增编号申请}

AI 应用场景

分布式训练中的通信死锁预防

阶段:模型训练与分布式资源调度

解决的问题:多机多卡训练时,各进程按任意顺序申请通信缓冲区和梯度锁,容易像四辆车一样互相等待。借鉴“资源编号递增申请”的思路,框架为设备与缓冲区统一编号,所有 rank 按编号递增顺序提交申请,从结构上切断环形等待,避免集群训练整体卡死。

具体案例:某大模型团队在 8 卡数据并行训练时,偶尔出现 all-reduce 卡住 30 秒的告警;排查确认是两个 rank 同时持有一个通信缓冲、又都等待对方释放最终梯度。改造为按 rank 编号排序后建立通信链路,死锁彻底消失,训练吞吐提升约 12%。

AI 推理服务的多线程锁排序

阶段:推理服务的并发控制

解决的问题:批量推理引擎中多个线程同时访问共享模型权重与结果队列,若线程 1 持锁 A 等锁 B、线程 2 持锁 B 等锁 A,服务会整体停顿。采用破坏循环等待的锁编号法,规定所有线程按锁 ID 递增加锁,杜绝锁顺序反转。

具体案例:某 NLP 在线服务的 batch 推理引擎曾因两级锁顺序不一致导致偶发 3 秒长停顿,按锁 ID 排序改造后,线上死锁类告警归零,P99 时延下降 8%。

特征工程数据库连接池的预分配

阶段:数据采集与特征存储

解决的问题:特征回填任务多线程同时取数据库连接做两段式更新时,线程可能持有连接 A 等待连接 B,另一线程反之。采用“一次性申请全部连接再执行、并设置获取超时”的预分配策略,使“占有并等待”无法同时成立。

具体案例:广告推荐系统的特征回填曾因连接池死锁导致数据延迟 1 小时;改造为 try-acquire 全量连接 + 超时释放后,任务稳定性显著提升,未再出现集体卡死。

Python 演示

python60 行
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""死锁检测与预防演示:等待图判环 + 编号递增申请"""  def build_wait_graph(procs):    """procs: {pid: (held, need)}, 返回等待图 {pid: [等待哪些pid]}"""    holder = {}    for pid, (held, need) in procs.items():        for r in held:            holder[r] = pid    graph = {pid: [] for pid in procs}    for pid, (held, need) in procs.items():        for r in need:            if r in holder and holder[r] != pid:                graph[pid].append(holder[r])    return graph  def has_cycle(graph):    """DFS 三色标记法检测环路"""    WHITE, GRAY, BLACK = 0, 1, 2    color = {pid: WHITE for pid in graph}     def dfs(pid):        color[pid] = GRAY        for nxt in graph[pid]:            if color[nxt] == GRAY:                return True            if color[nxt] == WHITE and dfs(nxt):                return True        color[pid] = BLACK        return False     return any(color[pid] == WHITE and dfs(pid) for pid in graph)  def show(name, procs):    graph = build_wait_graph(procs)    print(name)    print("  等待图:", graph)    print("  死锁?", "是" if has_cycle(graph) else "否")  def main():    # 场景一: P1 占 R1 要 R2, P2 占 R2 要 R1, 典型死锁    show("场景一: 交叉等待", {        "P1": (["R1"], ["R2"]),        "P2": (["R2"], ["R1"]),    })    # 场景二: 编号法预防 (R1 编号 1 < R2 编号 2)    # P2 想申请 R1 必须先释放 R2, 于是先释放再申请, 不构成反向持有    show("场景二: 编号法预防后", {        "P1": (["R1"], ["R2"]),        "P2": ([], ["R2"]),    })  if __name__ == "__main__":    main()
目的:用等待图 + DFS 环路检测模拟“四辆车互相等”的死锁,再演示资源编号法如何通过强制释放打破环路。
  1. holder[r] = pid登记每个资源当前被哪个进程占用,就像记下“每个路口被哪辆车占着”。
  2. graph[pid].append(holder[r])进程需要的资源被别人占着,就画一条“等待”边,完成等待图的构建。
  3. color[pid] = GRAYDFS 深入时把节点标记为灰色,表示“正在这条链上往下走”。
  4. if color[nxt] == GRAY: return True递归链上再次遇到灰色节点,说明绕回自己,找到环形等待。
  5. "P2": ([], ["R2"])编号法让 P2 先释放低编号资源,打断环路,死锁不再成立。

常见误区

误区资源分配图里只要出现一个环,系统就必然死锁了,所以画图找环就万事大吉。

正解这个结论只在“所有资源都是单实例”时成立。多实例资源下,环只是死锁的必要条件:环里某类资源如果还有空闲实例,等待的一方其实可以拿到资源继续推进,僵局会自然解开。就像四辆车的环路里,如果西车旁边恰好有一条空车道可以绕行,它就不会傻等,环虽然存在,堵死却不会发生。

为何常见:教材举例时为了直观,几乎都用单实例资源画图,看多了容易把“充分”当成“充要”。

误区四个必要条件排第一的就是互斥,那预防死锁自然优先从互斥下手,把它干掉最省事。

正解互斥是很多资源的物理属性,打印机不可能同时打印两份文件,强行共享只会制造数据混乱。实际工程中,“破坏互斥”应用面非常窄,通常只能靠假脱机这类“中间缓冲”间接实现;工业界真正常用的是破坏循环等待的资源编号法,因为它实现简单、对现有系统改动可控。

为何常见:清单顺序带来的“从第一条开始”的直觉,让人们高估了互斥的可改造性。

误区银行家算法能避免死锁,它应该和四种预防策略一样,都属于“预防”范畴吧?

正解银行家算法是死锁避免,不是预防。预防在运行前就用静态规则打破四个条件之一,不需要知道进程的未来需求;避免则在每次资源申请时动态检查系统是否处于安全状态,而它必须提前知道每个进程的最大需求量。打个比方:预防是“出门前把路修成不会堵的结构”,避免是“每次过路口前找交警确认能不能走”。

为何常见:教材把预防、避免、检测恢复放在同一章讲,概念边界被时间线压缩,容易被混为一谈。

误区既然占有并等待是死锁的元凶,那让进程一次性把自己的资源全要齐,不就一劳永逸了吗?

正解预分配确实能断掉“占着等”,但代价是资源利用率急剧下降:进程可能只用打印机十分钟,却要独占它整个生命周期。更隐蔽的是饥饿问题——低优先级进程可能永远凑不齐整套资源,被活活“饿死”。所以现实中预分配只用在事务短、需求明确的场景,而不是银弹。

为何常见:只盯着“死锁消除”这一个目标,忽略了系统整体效率和公平性,是初学者常见的单维度思维。

面试题

Q1. 下列哪一项属于死锁产生的必要条件?

单选题 · easy

  • A.抢占式调度、时间片轮转、优先级反转
  • B.互斥条件、占有并等待、不可剥夺、循环等待← 正确答案
  • C.共享内存、信号量、管程
  • D.同步、互斥、通信、并发

答案:B。四个必要条件缺一不可,就像四辆车在路口僵住需要“单车道、占道、不倒车、围成圈”四个条件同时满足。A 是调度概念,C 是同步工具,D 是并发系统的一般特性,都不是死锁的必要条件。

Q2. 破坏“占有并等待”条件通常采用的方法是?

单选题 · easy

  • A.进程启动前一次性申请所有需要的资源← 正确答案
  • B.允许进程随时抢占其他进程资源
  • C.为每个资源分配唯一编号并按编号递增申请
  • D.将独占资源改装成共享资源

答案:A。破坏占有并等待的经典做法是资源预分配:运行前一次性把全部资源要齐,执行期间不再申请,从而杜绝“占着等”的中间态。B 属于破坏不可剥夺,C 属于破坏循环等待,D 属于破坏互斥。

Q3. 在单实例资源的系统中,若资源分配图中存在一个环,则系统?

单选题 · mid

  • A.一定不会发生死锁
  • B.一定发生死锁← 正确答案
  • C.可能死锁也可能不会,需要银行家算法进一步判断
  • D.死锁与否与环无关

答案:B。单实例资源下,资源分配图中有环是死锁的充分必要条件。因为每个资源只有一个实例,环上的每个进程持有的资源都是环内另一个进程唯一需要的资源,谁也不会主动释放。多实例资源下才需要进一步化简判断。

Q4. 关于“破坏不可剥夺”策略,下列说法正确的是?

单选题 · mid

  • A.所有资源都适合剥夺式管理,因为回滚没有成本
  • B.CPU 这类状态易保存的资源可以剥夺,打印机这类状态难回滚的资源不适合← 正确答案
  • C.剥夺策略不需要保存任何现场信息
  • D.不可剥夺条件无法被任何手段破坏

答案:B。剥夺的前提是资源状态可保存、可恢复。CPU 寄存器中断后保存现场即可转给别人;打印机打了一半的文档没法回滚。就像交警能命令汽车倒车,但没法让打了一半的字消失重来。

Q5. 破坏循环等待条件的经典做法是?

单选题 · mid

  • A.运行前一次性申请全部资源
  • B.为资源类型线性排序,进程按编号递增顺序申请← 正确答案
  • C.允许进程按任意顺序申请资源
  • D.将资源改为共享设备

答案:B。资源编号法规定申请顺序严格递增,环路中必然存在一条编号下降的“回头”边,这条边被规则禁止,环自然无法形成,就像规定所有车只能朝大号路口开,永远绕不成圈。

Q6. 以下哪些属于死锁预防策略?

多选题 · mid

  • A.破坏互斥条件← 正确答案
  • B.破坏占有并等待条件← 正确答案
  • C.银行家算法
  • D.鸵鸟算法

答案:AB。预防策略的本质是运行前用静态规则打破四个必要条件之一,因此破坏互斥与破坏占有等待都属于预防。银行家算法是运行期动态检查安全状态的“避免”策略;鸵鸟算法则是“不处理”的态度,两者都不是预防。

Q7. 关于破坏互斥条件,下列说法正确的是?

单选题 · mid

  • A.互斥条件可以轻松消除,比如把打印机改成共享设备
  • B.互斥是很多资源的固有属性,通常只能靠假脱机技术间接绕开← 正确答案
  • C.破坏互斥是工业界最常用的死锁预防策略
  • D.取消互斥后系统不需要任何同步机制了

答案:B。打印机等设备物理上就是独占的,硬改会造成数据混乱,工程上多用假脱机技术让输出先进磁盘队列、再由专门进程统一打印,进程“以为”打印机是共享的。常用预防策略其实是破坏循环等待的编号法。

Q8. 关于死锁预防与死锁避免的区别,以下说法正确的有?

多选题 · hard

  • A.预防用静态约束在运行前打破四条件之一,不需要未来信息← 正确答案
  • B.避免在每次资源申请时动态判断系统是否处于安全状态← 正确答案
  • C.预防只适用于单实例资源,避免只适用于多实例资源
  • D.两者效果完全相同,只是实现方式不同

答案:AB。预防是“事前修路”,用规则强行拆掉死锁条件;避免是“事中看路”,依赖银行家算法等动态安全检测,且需要知道每个进程的最大需求。C 与 D 的限定和说法都是错误的。

Q9. 某系统采用资源顺序编号法(编号 1、2、3),进程 A 已持有资源 2,现在想申请资源 1。按“递增顺序申请”的规则,正确的做法是?

单选题 · hard

  • A.直接申请资源 1,因为当前资源空闲
  • B.必须先释放资源 2,再申请资源 1,避免反向持有← 正确答案
  • C.同时申请资源 1 和资源 2
  • D.修改资源 1 的编号让它大于资源 2

答案:B。编号法要求申请方向只能由小到大。持有 2 号却想要 1 号属于“回头”申请,必须先把 2 号释放掉再申请 1 号。这套规则就是靠禁止反向申请来杜绝环路,就像规定车辆只能往大号路口开。

Q10. 关于资源分配图与死锁的关系,以下说法正确的有?

多选题 · extreme

  • A.单实例资源系统中,存在环路是死锁的充分必要条件← 正确答案
  • B.多实例资源系统中,存在环路是死锁的必要条件← 正确答案
  • C.只要存在环路就一定死锁,与资源实例数量无关
  • D.资源分配图无环时,系统一定不会发生死锁← 正确答案

答案:ABD。单实例下环等价于死锁;多实例下环只是必要条件,因为环内可能有空闲实例化解僵局;无环则说明没有循环等待,死锁不可能成立。C 的错误在于忽略了多实例资源的“救场”能力,就像环路里还有一条空车道可绕行。