Theory · OS · Deadlock

死锁

四个必要条件 → 四种处理策略 —— 从教科书银行家算法到 MySQL/Go 里的真实死锁现场

理论骨架

互斥、占有并等待、不可剥夺、循环等待——四条同时成立才死锁,破一即活

策略光谱

鸵鸟(不处理)→ 预防(破坏条件)→ 避免(银行家)→ 检测恢复(事后补救),约束越强开销越大

工程现实

真实系统几乎都选"预防 + 检测":锁排序写进规范,数据库内置死锁检测回滚,Go 用 runtime 崩溃暴露死锁

定位:OS 系列第六篇,上承 thread-sync 的锁语义。主线:"四条件是判定标准,四策略是应对菜单,工程落地是预防为主 + 检测兜底"。

Why Deadlock Matters

先看现象:两段都对的代码,合起来就永远卡住

// 两个 goroutine,两把锁;单独看谁都没写错
var muA, muB sync.Mutex

// G1:先拿 A,再拿 B
go func() {
    muA.Lock()
    muB.Lock()      // ← 停住:B 在 G2 手上
    doWork()
    muB.Unlock(); muA.Unlock()
}()

// G2:先拿 B,再拿 A(顺序反了)
go func() {
    muB.Lock()
    muA.Lock()      // ← 停住:A 在 G1 手上
    doWork()
    muA.Unlock(); muB.Unlock()
}()
关键观察:问题不在任何一行代码,而在两个执行流拿锁的顺序不一致。这是结构性缺陷,靠"再加个 sleep""重试一次"都修不好。

卡住的四个瞬间

t0 G1 拿到 A,G2 拿到 B —— 一切正常
t1 G1 想要 B(被 G2 占着)→ 停住等;G2 想要 A(被 G1 占着)→ 停住等
t2 G1 等的是 G2 手里的东西,G2 等的是 G1 手里的东西 —— 互相等,而且谁也不会先放手
t3 永久停住。注意不是"慢",是永远:没有任何外部事件能把它们唤醒

为什么它值得单独学一章

它不会自愈:数据竞争、偶发超时这类 bug 重试一下可能就绕过去了;死锁一旦成立就是 100% 停在那,只能人为介入。
它完全可预测:只要四个条件同时成立,死锁必然发生——所以它有标准化的判定方法和应对菜单,不需要玄学调试。
面试与线上双高频:从"四条件"一直问到"MySQL 报 1213 怎么办",既是必答题,也是线上 P0 的常见来源。

本 deck 的路线

先给死锁一个能判定的定义(四条件)→ 再给一套应对菜单(四策略)→ 最后落到数据库与 Go 的真实现场。读完你应该能回答:"它为什么必死"以及"我该选哪个策略"。

动机页:先用一段可运行的最小代码让读者"看到"死锁,再说明它不是偶发 bug 而是结构性必然。四个瞬间按时间线推进,落到"不会自愈 + 可预测 + 高频"三条学习理由。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
资源一次只允许一个执行流使用的东西:一把锁、一行数据库记录、一台打印机
执行流"正在跑的一段程序"的统称——本 deck 里进程、线程、goroutine 都算,谁在等谁才是重点
临界区加锁和解锁之间的那段代码,同一时刻只允许一个执行流在里面
互斥保证"同一时刻只有一个执行流能进临界区"的性质
阻塞想要的东西拿不到,就停下来等;停着的时候不消耗 CPU
可剥夺资源能不能被系统从持有者手里强行收走再给别人;锁不行,内存可以
事务数据库里"要么全做完、要么全不做"的一组操作
回滚把已经做过的操作撤销,退回到之前的状态——事务天生支持
等待图一张画"谁在等谁"的图:A→B 表示 A 在等 B 持有的资源;图里有环 = 可能死锁

如果你还不确定"锁"是什么

先读这两篇再回来,本 deck 默认你已经知道锁的基本用法:
同步与互斥 → 锁、信号量、条件变量怎么用
进程 · 线程 · 协程 → "谁在等"里的"谁"到底是谁

本 deck 怎么用这些词

为了不纠结名词,后面统一说执行流在争抢资源;只有涉及具体系统时才换成"事务""goroutine"。你只需要记住一件事:死锁是两个以上的执行流,互相攥着对方要的东西,且谁都不肯先放手。

一个提前建立的直觉

既然死锁需要"四个条件同时成立",那应对思路天然分成两类:让条件凑不齐(预防/避免,死锁不可能发生),或者让它发生然后拆掉(检测/恢复)。后面四策略就是这两类的展开。

阅读提示:术语不用背,遇到忘了的回来查这一页就行;真正需要背的只有第 4 页的四个条件和最后那张速查表。
前置页:九个术语先定义再使用,杜绝"如你所知"式跳跃。右侧给出两条前置 deck 链接和一个提前建立的二分直觉(凑不齐条件 vs 发生后拆掉),为第 5 页的策略总览铺路。

Coffman Conditions

从刚才的例子到定义:四个必要条件,缺一不可

开场那段程序"必死"不是运气差——它同时满足了四个条件。四个条件由 Coffman 等人在 1971 年给出:四个同时成立才死锁,破坏任意一个就不可能死锁

资源分配图:两个进程循环等待形成死锁 资源分配图:进程 P1 持有资源 R1 并请求 R2,进程 P2 持有资源 R2 并请求 R1,两条请求边构成一个循环,单实例资源下循环即死锁。右侧列出四个必要条件:互斥、占有并等待、不可剥夺、循环等待,并注明破坏任意一个即可解除死锁。 P1 P2 R1 R2 持有 持有 请求 R2 请求 R1 单实例资源:请求边成环 = 死锁(P1 等 P2 放 R2,P2 等 P1 放 R1) 多实例资源:有环不一定死锁,但死锁必有环 四个必要条件(Coffman) ① 互斥:资源一次只能一人用 锁、DB 行锁、打印机——共享可读的就没这问题 ② 占有并等待:拿着旧的,还要新的 持 A 锁时去申请 B 锁——嵌套锁是温床 ③ 不可剥夺:资源只能自愿释放 mutex 不能被第三方抢走送人 ④ 循环等待:等待关系成环 P1→P2→P3→P1 的等待链闭合 推论(必背): 四个条件同时成立才会死锁;破坏任意一个, 死锁就不可能发生 —— 这是"预防"策略的全部依据
资源分配图:请求边(进程→资源)、分配边(资源→进程)。单实例环=死锁、多实例有环未必死锁是精确性考点。四条件的"缺一不可"性是必背推论。

Ostrich · Prevention · Avoidance · Detection

四种处理策略:约束与开销的光谱

策略思路代价现实采用
鸵鸟策略假装看不见:死锁概率极低时重启解决零设计成本;出事靠人肉多数通用 OS(Linux/Windows)对用户态进程的态度
死锁预防静态设计:破坏四条件之一,让死锁不可能发生资源利用率低(一次性申请)、吞吐受损工程规范:锁排序、trylock;最常用
死锁避免动态判断:每次分配前确认仍处安全状态(银行家算法)要预知最大需求 + 每次分配做 O(m·n²) 检查教科书多、现实少(需求难预知)
检测 + 恢复放任发生:周期性跑死锁检测,发现后回滚/抢占/杀进程检测开销 + 回滚损失(事务白做)数据库:InnoDB 等待图检测 + 回滚代价小的事务
为什么 OS 选鸵鸟:进程死锁只死自己(OS 层有隔离),资源不共享到那个程度;而"预防"会伤所有正常程序的性能。数据库选检测:事务天然可回滚,恢复成本低——策略选择 = 恢复成本的函数
面试金句:"四策略是一条光谱:约束越强、死锁越不可能,但资源利用率越低。工程答案是组合拳——预防写进编码规范(锁排序),检测交给数据库内核。"
总览页给"策略 = 恢复成本函数"的视角:OS 恢复死锁进程太贵所以鸵鸟,DB 回滚事务便宜所以检测。这张表是 QA 高频题的完整答案模板。

Prevention: Break One Condition

预防:破坏四个条件中的任意一个

破"互斥"—— 通常做不到

资源本性决定(写锁必须独占)。可行的间接做法:把独占资源改造成共享/虚拟化(假脱机打印队列把打印机变成队列)——只对特殊资源成立。

破"占有并等待"—— 一次性申请

开始工作前一次性申请全部资源,拿不齐就全释放重试。代价:利用率低(很多资源闲着被占)、可能饥饿(凑不齐一直重试)。工程变体:进入大事务/复杂流程前集中拿锁。

破"不可剥夺"—— 抢占式

申请新资源失败时,释放已持有的(或被系统强制剥夺)。要求资源状态可保存恢复——锁做不到(临界区状态复杂),事务可以(回滚即恢复)。

破"循环等待"—— 资源有序 ★ 最实用

给全部锁全局编号,所有人只能按升序申请;持有高号锁时不得再申请低号锁——环在数学上不可能闭合。哲学家"奇偶分组"、Go 的"map+slice 不嵌套锁规范"都是它的实例。

// 资源有序的最小实现:按地址排序加锁
func LockTwo(a, b *sync.Mutex) {
    if uintptr(unsafe.Pointer(a)) >
       uintptr(unsafe.Pointer(b)) {
        a, b = b, a      // 先低后高
    }
    a.Lock()
    b.Lock()
}
// 全进程内所有两锁路径都走它
// → 循环等待条件被破坏

// trylock 变体:拿不齐就退回
if !b.TryLock() {
    a.Unlock()  // 拿不到 B 就放 A 重试
    time.Sleep(...)
}
锁排序的工程化:方案一全局顺序(按锁的用途/ID 编号);方案二按对象地址(如上,免维护但顺序隐式);方案三分层锁(L1 不持有时才能拿 L2)。规范 + 静态检查(如 Go 的 lockguard 类 linter 思路)双保险。
面试标准句:"四个条件里互斥最难破、循环等待最容易破——所以工程上 'fix the lock ordering' 是死锁预防的同义词。"
四个破坏方案按实用性排序:循环等待(锁排序)> 占有并等待(一次性申请)> 不可剥夺(抢占)> 互斥(几乎不可破)。右侧"按地址排序"是无编号场景的通用技巧。

Avoidance · Banker's Algorithm

避免:银行家算法与安全状态

安全状态

存在一个调度序列让所有进程都能拿到最大需求并完成——系统就处于安全状态。安全 ≠ 不死锁的超集关系:不安全状态可能死锁,安全状态必然不死锁。避免策略 = 每次分配前检查"分配后是否仍安全",不安全就拒绝或让进程等待。

银行家算法流程

每个进程预先声明最大需求 Max。分配请求来时:① 请求 ≤ 剩余需求;② 请求 ≤ 可用资源;③ 试分配后跑安全性检查:在剩余资源里找"剩余需求都能被满足"的进程,假装它完成并归还资源,重复直到全部完成——找不到则回滚本次分配。

为什么现实少用

三个硬前提:① 进程要预知 Max(业务写不出"最多用多少内存");② 进程数与资源固定;③ 每次分配都要 O(m·n²) 检查。它证明了"动态避免可行",但只适合资源类型少、需求可声明的封闭系统。

进程已分配最大需求还需
P1297
P2341
P3275

手算示例:总资源 10,当前可用 3

可用 3 ≥ P2 还需 1 → 满足 P2,P2 完成归还已分配的 3 份 → 可用 3+3=6;6 ≥ P3 还需 5 → P3 完成归还 2 份 → 可用 6+2=8;8 ≥ P1 还需 7 → P1 完成。安全序列 <P2, P3, P1> 存在 → 安全。若把剩余 3 份全部借给 P1(可用变 0),P2 还需 1、P3 还需 5 都无法满足,谁也完不成 → 不安全,银行家会拒绝该请求。

面试口径:"银行家算法 = '借出去之后,大家还能都拿到所需并还清吗'的模拟检查。答完机制补一句它的现实局限(Max 不可知),比硬背公式高一档。"
安全序列的直觉:"总有办法让所有人毕业"。示例要现场推一遍安全序列;"不安全≠必死锁"是精确性考点。数字示例简化为单资源类型,降低记忆负担。

Detection & Recovery

检测与恢复:放任发生,事后拆弹

怎么检测

维护等待图(进程→所等进程的边,由资源分配图化简而来):单实例资源,图中有环即死锁;多实例则要做资源分配图的可简化性检查(不断找"需求都能满足"的进程消去,剩不下图即无死锁)。周期性或"请求长时间不满足"时触发——数据库就是后者。

怎么恢复(三选一或组合)

剥夺资源:强制抢占,把资源给别的进程(需要状态可保存);② 进程回滚:回退到检查点重跑(事务天然支持——回滚即恢复);③ 终止进程:杀一个或全部环上进程,选代价最小的(运行时间短、产出少、优先级低的先杀)。

饥饿的次生问题

恢复时"总是杀同一个倒霉蛋"会造成该进程饥饿——现实实现会在选择受害者时把回滚次数计入代价(InnoDB 的受害者权重考量类似)。

等待图检测:环即死锁 等待图:P1 指向 P2 表示 P1 等待 P2 持有的资源,P2 指向 P1 形成环;检测器发现环后选择回滚代价较小的事务 T2 打破死锁。 T1 T2 T3 T1 等 T2 的行锁 T2 等 T3 T3 等 T1 的行锁 检测器动作 发现环 T1→T2→T3→T1 选受害者: undo 量最小的 回滚 T2 → 环断开 其余照常跑 InnoDB:innodb_deadlock_detect 默认开启,秒级发现、报 1213 错误
面试金句:"检测策略的本质是把死锁当成可重试的运行时错误——前提是有便宜且安全的恢复手段(事务回滚),没有这个前提就老老实实预防。"
等待图 = 简化后的资源分配图(把资源节点折叠掉)。InnoDB 是"检测+恢复"的工业标杆案例:innodb_deadlock_detect 默认开、报 1213。受害者选择计入回滚次数防饥饿。

Deadlock · Livelock · Starvation

三兄弟辨析:卡住、忙活、排不上

维度死锁活锁饥饿
状态互相等待,阻塞不动一直在动(让路重试),但无进展其他人在跑,自己排不上
CPU 占用低(睡眠等待)(忙着重试)取决于谁在跑
典型成因循环等待(嵌套锁)谦让式重试:两个线程同时退避再同时重试无公平性的调度/锁(读者饿写者)
解法破坏四条件 / 检测回滚随机退避(指数 backoff + jitter)FIFO 队列、老化提权、公平锁
记忆锚点:死锁是"堵车熄火",活锁是"两人在走廊互相让路、步调一致地永远让下去",饥饿是"公交永远挤不上"。

活锁的现实场景

trylock 重试风暴:A 拿 1 失败 2,B 拿 2 失败 1,双双释放重试且节奏同步——固定 sleep 会撞车,随机化退避是标准解;② 分布式系统的选举/重试协议风暴;③ 消息处理失败立即重投,两个消费者来回踢皮球。

饥饿的现实场景

① 读写锁读者川流不息饿死写者(Go RWMutex 用"写者阻塞新读者"解决);② 优先级队列无老化,低优先级永远轮空;③ 公平性缺失的内核调度(CFS/EEVDF 的 lag 机制本质就是反饥饿)。

一个判定技巧

状态:阻塞不动 = 死锁或饥饿(有环死锁、无环饥饿);在动但无进展 = 活锁。看CPU:忙转 = 活锁;闲挂 = 死锁/饥饿。两问定位三兄弟。

对比表 + 两个判定问题(状态动不动、CPU 忙不忙)。随机退避(jitter)是活锁的万金油解,分布式重试同样适用。

Deadlock in the Wild

工程现场:数据库、Go、分布式里的死锁

MySQL/InnoDB:检测派(高频考点)

两个事务交叉更新两行即经典死锁:T1 锁行 A 求行 B,T2 锁行 B 求行 A。InnoDB 默认开启等待图检测(innodb_deadlock_detect),秒级发现并回滚 undo 量小的事务,客户端收到 1213 Deadlock found——应用应捕获并重试。高并发热点行还有优化点:关闭检测配合 innodb_lock_wait_timeout 兜底(超时派),减少检测开销。

Go:两类"死锁"表现不同

sync 死锁:所有 goroutine 都睡在锁/channel 上 → runtime 检测到 all-asleep,直接 fatal error: all goroutines are asleep - deadlock! 崩溃暴露;② channel 死锁:无缓冲 channel 自己发自己收、对 nil channel 收发——同样触发。但部分 goroutine 互相等待(还有人活着)runtime 不管,要靠 pprof goroutine 画像排查。

// InnoDB 死锁的标准应用侧姿势
for i := 0; i < 3; i++ {
    err := tx.UpdateTwoRows(a, b)
    if mysql.IsDeadlock(err) {
        time.Sleep(time.Millisecond *
            time.Duration(rand.Intn(50)))
        continue  // 随机退避后重试
    }
    break
}
// 预防侧:统一按主键顺序更新
// (两个事务都先改 id 小的行)
预防清单(评审 checklist):① 锁全局排序;② 缩小临界区,锁内不做 I/O;③ 少用嵌套锁(必须嵌套时按序);④ 外部系统调用移出锁外;⑤ 数据库批量操作按主键排序;⑥ 死锁错误要有重试路径。
面试金句:"线上死锁三板斧:预防靠排序、发现靠检测/画像、善后靠重试——Go 的 all-asleep 崩溃和 MySQL 的 1213 是两个生态给的现成答案。"
两个工业案例对齐第 3 页的策略表:DB=检测恢复(回滚便宜),Go=runtime 级检测(部分死锁不报要靠画像)。重试代码里"随机退避"同时防活锁。

Cheat Sheet

一页带走:判定、预防、选型、排查

① 判定:是不是死锁

四条件同时成立互斥 + 占有并等待 + 不可剥夺 + 循环等待 —— 缺一即不可能死锁
单实例资源等待图有环 ⇔ 死锁
多实例资源有环未必死锁,但死锁必有
银行家视角安全状态 ⇒ 不会死锁;不安全状态可能死锁

② 预防:破哪个条件最划算

循环等待 ★★★锁全局排序(按 ID / 用途 / 对象地址),升序申请 —— 最实用
占有并等待 ★★一次性申请全部资源;代价是利用率低、可能饥饿
不可剥夺 ★★TryLock 拿不到就放手重试;必须配随机退避防活锁
互斥把独占资源改造成共享(假脱机队列),只对特殊资源可行

③ 策略选型:看恢复成本

通用 OS 用户进程只能杀进程,代价大 → 鸵鸟(假装看不见)
数据库事务回滚即可,代价小 → 检测 + 回滚
自己的业务代码出事要人肉查 → 预防为主(锁排序写进规范)
资源可声明上界的封闭系统能预知 Max → 银行家避免(教科书为主)

④ 三兄弟:两问定位

在动吗不动(阻塞、CPU 闲)= 死锁或饥饿;在动但无进展(CPU 忙)= 活锁
有环吗互相等待成环 = 死锁;不成环只是轮不到 = 饥饿

⑤ 线上三板斧

预防靠排序 · 发现靠检测/画像 · 善后靠重试。
· 写码:锁全局排序、临界区最小化、锁内不做 I/O
· 发现:数据库靠等待图检测(MySQL 报 1213),Go 靠 pprof goroutine 画像
· 善后:捕获死锁错误 → 随机退避 → 整体重做事务
一句话背下来:死锁不是"运气不好",是四个条件被同时满足;所以要么让它凑不齐(排序),要么让它发生后能便宜地拆掉(回滚)。
速查页是"可检索性优于一次性"原则的落点:回看时只看这一页。五块按使用顺序排——先判定、再预防、再选型、再辨析、最后排查处置。

Interview QA · Part 1

高频追问:理论与算法

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回速查页。

1 · 死锁的四个必要条件?

Coffman

互斥、占有并等待、不可剥夺、循环等待——同时成立才死锁,破坏任一即不可能。追问"哪个最容易破坏":循环等待(锁排序),哪个最难:互斥(资源本性)。

2 · 怎么处理死锁?各有什么代价?

四策略光谱

鸵鸟(零成本、靠重启)、预防(破坏条件,伤利用率)、避免(银行家,要预知 Max + 每次分配检查)、检测恢复(放任发生事后回滚/杀进程,需要便宜恢复手段)。策略 = 恢复成本的函数。

3 · 银行家算法的核心思想?

安全状态

每次分配前模拟:剩下的资源能否支持某个完成序列让所有进程毕业(安全序列)。安全则分配,不安全则拒绝。局限:Max 不可预知、检查开销 O(m·n²),现实少用。

4 · 不安全状态一定会死锁吗?

必要非充分

不一定。不安全 = 找不到保证所有人都完成的安全序列,但进程实际需求可能小于声明(Max 是上界),可能仍然顺利跑完。安全状态必不死锁,不安全状态可能死锁——精确性考点。

5 · 死锁检测怎么实现?

等待图

把资源分配图化简成等待图(进程间"谁等谁"):单实例资源有环即死锁;多实例需做可简化性检查。实现上周期性跑,或请求等待超阈值时触发(InnoDB 的做法)。

6 · 死锁、活锁、饥饿的区别?

状态三态

死锁:阻塞互等无进展;活锁:不停重试无进展(忙);饥饿:别人有进展自己排不上。判定:看是否阻塞 + 看是否忙。解法各不同:破坏条件/检测、随机退避、公平调度。

理论组六题覆盖四条件、四策略、银行家、安全状态、检测、三兄弟辨析。第 4 题的"必要非充分"是最容易被追问的精确性细节。

Interview QA · Part 2

高频追问:工程实战

同样建议先自答。这一页的题都需要"结论 + 一句代价/边界"才完整——只答结论会被追问。

7 · 写代码时怎么预防死锁?

锁排序checklist

核心是破坏循环等待:锁全局排序(按 ID/用途/对象地址)。配套:临界区最小化、锁内不做 I/O、能不嵌套就不嵌套、拿不到用 TryLock 退避、死锁错误路径必配重试。

8 · MySQL 死锁报错怎么处理?

1213 + 重试

InnoDB 默认自动检测并回滚其中一个事务,客户端收 1213。应用侧:捕获该错误 → 随机退避 → 重试(事务要整体重做)。预防侧:批量更新按主键排序、事务尽量短、热点行考虑排队更新。

9 · Go 的 fatal error: deadlock 什么时候触发?

all goroutines asleep

所有 goroutine 都阻塞(锁/channel/syscall 等待)且没有 timer/netpoller 唤醒来源时,runtime 判定全局死锁直接崩溃。注意:只是子集互等时 runtime 不报——用 pprof 的 goroutine profile 排查。

10 · 两个 goroutine 互发无缓冲 channel 会怎样?

channel 死锁

ch := make(chan int) 后同一 goroutine 写 ch 再读 ch:发送阻塞等接收者,但接收者就是自己——永远等不到,若全体 goroutine 如此则触发 all-asleep 崩溃;解法:带缓冲、另一 goroutine 消费,或 select + default。

11 · 分布式系统还有死锁吗?

跨节点等待环

有:A 节点的事务等 B 节点持有的资源,B 又等 A——跨进程/跨网的资源分配图。单机检测看不到全局环,需要分布式死锁检测或设计上规避:资源全序、超时重试(把死锁退化为可重试失败)、Saga 补偿。

12 · 死锁和资源泄漏导致的"卡死"怎么区分?

排查路径

先看状态:抓 goroutine/线程 dump,若互相阻塞成环(彼此的栈都在对方持有的锁上)= 死锁;若都在等外部资源(连接池耗尽、下游不回)= 依赖卡死。Go 用 pprof goroutine + 按等待原因分组;通用手段:超时兜底让问题显性化。

工程组六题:预防清单、MySQL 1213、Go all-asleep 边界、channel 死锁、分布式死锁、卡死排查。第 11 题把死锁概念推广到分布式(全序/超时/Saga)。

Related & References

相关知识点与参考

OS 系列(本分类)

同步与互斥 →(死锁的原料:锁)
CPU 调度 →(饥饿与公平调度的对抗)
进程线程协程 →(谁在等:等待主体)

跨领域联动

InnoDB 锁体系 →(等待图检测的工业实现)
sync 并发原语 →(Go 侧锁与 all-asleep 检测)
分布式锁 →(跨节点死锁与超时设计)

参考来源(本 deck 结论可溯源至下列一手材料)

OSTEP ch.32(Deadlock)四条件、四策略框架、银行家算法与安全状态
Coffman et al., 1971(System Deadlocks)四个必要条件的原始出处
MySQL 8.0 Reference Manual: InnoDB Locks / Deadlocks等待图检测、1213 错误、innodb_deadlock_detect 语义
Go src/runtime/proc.go(gopark / all-asleep check)全局死锁判定与 fatal error 触发条件
Dijkstra, 1965(哲学家就餐原始论文)资源分层/有序解法的历史源头
xiaolincoding.com《图解系统》deadlock.html死锁处理策略的中文叙述
收尾:Coffman 1971 与 OSTEP ch32 是理论源,MySQL/Go 是两个工程实现锚点。总页数 14。CPU 侧进程管理到此收官,下一篇进入内存管理主线。