diff --git a/notes/pics/deadlock-banker-multi.png b/notes/pics/deadlock-banker-multi.png new file mode 100644 index 00000000..95c57b6d Binary files /dev/null and b/notes/pics/deadlock-banker-multi.png differ diff --git a/notes/pics/deadlock-banker-single.png b/notes/pics/deadlock-banker-single.png new file mode 100644 index 00000000..f11d25fb Binary files /dev/null and b/notes/pics/deadlock-banker-single.png differ diff --git a/notes/pics/deadlock-detect-graph.png b/notes/pics/deadlock-detect-graph.png new file mode 100644 index 00000000..4fc166d7 Binary files /dev/null and b/notes/pics/deadlock-detect-graph.png differ diff --git a/notes/pics/deadlock-detect-matrix.png b/notes/pics/deadlock-detect-matrix.png new file mode 100644 index 00000000..643e2907 Binary files /dev/null and b/notes/pics/deadlock-detect-matrix.png differ diff --git a/notes/pics/deadlock-safe-state.png b/notes/pics/deadlock-safe-state.png new file mode 100644 index 00000000..bb8da385 Binary files /dev/null and b/notes/pics/deadlock-safe-state.png differ diff --git a/notes/pics/deadlock-transfer.png b/notes/pics/deadlock-transfer.png new file mode 100644 index 00000000..f6deebb3 Binary files /dev/null and b/notes/pics/deadlock-transfer.png differ diff --git a/notes/计算机操作系统 - 死锁.md b/notes/计算机操作系统 - 死锁.md index 2b81337b..f7c8ab44 100644 --- a/notes/计算机操作系统 - 死锁.md +++ b/notes/计算机操作系统 - 死锁.md @@ -17,12 +17,16 @@ * [1. 安全状态](#1-安全状态) * [2. 单个资源的银行家算法](#2-单个资源的银行家算法) * [3. 多个资源的银行家算法](#3-多个资源的银行家算法) + * [程序中的死锁](#程序中的死锁) + * [1. Java 线程死锁](#1-java-线程死锁) + * [2. MySQL 死锁](#2-mysql-死锁) + * [参考资料](#参考资料) ## 必要条件 -

+

- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 - 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 @@ -54,26 +58,26 @@ ### 1. 每种类型一个资源的死锁检测 -

+

上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。 -图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。 +图中 P1、P2、P3 和 R1、R2、R3 构成一个环,满足环路等待条件,因此发生了死锁。 每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。 ### 2. 每种类型多个资源的死锁检测 -

+

-上图中,有三个进程四个资源,每个数据代表的含义如下: +上图中,有三个进程三种资源,每个数据代表的含义如下: - E 向量:资源总量 - A 向量:资源剩余量 - C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 - R 矩阵:每个进程请求的资源数量 -进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。 +进程 P1 和 P3 所请求的资源都得不到满足,只有进程 P2 可以,让 P2 执行,之后释放 P2 拥有的资源,此时 A = (3 1 2)。P1 可以执行,执行后释放 P1 拥有的资源,A = (4 2 2)。P3 也可以执行。所有进程都可以顺利执行,没有死锁。 算法总结如下: @@ -115,9 +119,9 @@ ### 1. 安全状态 -

+

-图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。 +图中条形的长度表示进程总共需要的资源数(Max),浅蓝部分表示已拥有的资源数(Has),Free 表示还可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源,运行结束后释放 B,此时 Free 变为 6(图 b);接着以同样的方式运行 A 和 C,使得所有进程都能成功运行,因此可以称图 a 所示的状态是安全的。 定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。 @@ -127,20 +131,96 @@ 一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 -

+

-上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 +上图 b 为不安全状态,因此算法会拒绝 C 的这次申请,从而避免进入图 b 中的状态;图 c 仍然是安全的,可以分配。 ### 3. 多个资源的银行家算法 -

+

-上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。 +上图中有四个进程,三种资源。Allocation 表示已经分配的资源,Need 表示还需要分配的资源。E 和 Available 分别表示总资源和可用资源,它们都是向量,而不是具体数值,例如 Available = (1 1 2) 表示 3 种资源分别还剩下 1、1、2 个。 检查一个状态是否安全的算法如下: -- 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。 -- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 +- 查找 Need 矩阵中是否存在一行小于等于向量 Available。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。 +- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 Available 中。 - 重复以上两步,直到所有进程都标记为终止,则状态是安全的。 如果一个状态不是安全的,需要拒绝进入这个状态。 + +## 程序中的死锁 + +### 1. Java 线程死锁 + +两个线程反向转账时,T1 锁住 alice 再请求 bob,T2 锁住 bob 再请求 alice,就是开头图中的情况: + +```java +static void transfer(Account from, Account to, int amount) { + synchronized (from) { + synchronized (to) { + from.balance -= amount; + to.balance += amount; + } + } +} +``` + +用 jstack 查看进程的线程栈,末尾会直接报告死锁(省略了部分内容): + +```text +Found one Java-level deadlock: +============================= +"T1": + waiting to lock monitor 0x0000000135f26a70 (object 0x000000061fc14e18, a Transfer$Account), + which is held by "T2" + +"T2": + waiting to lock monitor 0x0000000125f06e60 (object 0x000000061fc14dd0, a Transfer$Account), + which is held by "T1" +``` + +解决方法是破坏环路等待:所有线程按同一个顺序加锁,例如总是先锁 id 小的账户。 + +```java +Account first = from.id < to.id ? from : to; +Account second = from.id < to.id ? to : from; +synchronized (first) { + synchronized (second) { + from.balance -= amount; + to.balance += amount; + } +} +``` + +两个线程各反向转账 10 万次,原来的写法每次运行都会卡住,按 id 顺序加锁后都能完成,两个账户的余额仍为 1000。使用 ReentrantLock 时还可以用 tryLock 设置超时,拿不到锁就释放已持有的锁再重试,破坏的是占有和等待条件。 + +### 2. MySQL 死锁 + +事务按不同的顺序更新同一组行时也会死锁。例如两个事务同时反向转账,事务 1 先更新 id = 1,事务 2 先更新 id = 2,之后各自更新对方已经锁住的行: + +```sql +-- 事务 1 +BEGIN; +UPDATE account SET balance = balance - 100 WHERE id = 1; +UPDATE account SET balance = balance + 100 WHERE id = 2; -- 等待事务 2 释放 id = 2 的行锁 + +-- 事务 2 +BEGIN; +UPDATE account SET balance = balance - 50 WHERE id = 2; +UPDATE account SET balance = balance + 50 WHERE id = 1; +-- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction +``` + +InnoDB 默认开启死锁检测,发现环后立即回滚其中一个事务(倾向于回滚修改行数少的事务),另一个事务继续执行。上例中事务 2 被回滚,事务 1 提交后余额为 900 和 1100。执行 SHOW ENGINE INNODB STATUS,在 LATEST DETECTED DEADLOCK 一节中可以看到两个事务各自持有和等待的锁。被回滚的事务需要由应用重试。 + +减少数据库死锁的方法: + +- 按相同的顺序访问行,例如转账时先更新 id 小的账户; +- 事务尽量短,不在事务中调用外部接口或等待用户操作; +- 为更新的条件列建立索引,避免扫描时锁住过多的行。 + +## 参考资料 + +- Tanenbaum A S, Bos H. Modern Operating Systems [M]. Pearson, 2014. +- [Deadlocks in InnoDB | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks.html) diff --git a/tools/diagram/src/deadlock-banker-multi.svg b/tools/diagram/src/deadlock-banker-multi.svg new file mode 100644 index 00000000..a02a7961 --- /dev/null +++ b/tools/diagram/src/deadlock-banker-multi.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + 多个资源的银行家算法:找出一个安全序列 + E 总量 + + 9 + + 5 + + 7 + Available + + 1 + + 1 + + 2 + Allocation 已分配 + P1 + + 2 + + 1 + + 1 + P2 + + 3 + + 0 + + 2 + P3 + + 1 + + 2 + + 1 + P4 + + 2 + + 1 + + 1 + Need 还需要 + P1 + + 3 + + 2 + + 2 + P2 + + 1 + + 0 + + 2 + P3 + + 5 + + 2 + + 0 + P4 + + 0 + + 1 + + 1 + 安全序列 P2 → P4 → P1 → P3 + + 1. P2 需要 (1 0 2) ≤ (1 1 2) + 归还后 Available = (4 1 4) + + 2. P4 需要 (0 1 1) ≤ (4 1 4) + 归还后 Available = (6 2 5) + + 3. P1 需要 (3 2 2) ≤ (6 2 5) + 归还后 Available = (8 3 6) + + 4. P3 需要 (5 2 0) ≤ (8 3 6) + 归还后 Available = (9 5 7) + Need = 最大需求 − 已分配。一开始只有 P2 的需求能被满足,P1 缺 R2、P3 缺 R1; + 收到新的申请时,先假设分配,再用这个方法检查,找不到安全序列就拒绝。 + diff --git a/tools/diagram/src/deadlock-banker-single.svg b/tools/diagram/src/deadlock-banker-single.svg new file mode 100644 index 00000000..eac2d55f --- /dev/null +++ b/tools/diagram/src/deadlock-banker-single.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + 单个资源的银行家算法:共 12 个,分配前先检查分配后是否安全 + (a) 当前状态 + A + + + 3 / 8 + B + + + 2 / 4 + C + + + 4 / 9 + + Free 3 + 安全:B → A → C + (b) C 再申请 2 个 + A + + + 3 / 8 + B + + + 2 / 4 + C + + + 6 / 9 + + Free 1 + 不安全:谁都补不齐,拒绝 + (c) B 再申请 1 个 + A + + + 3 / 8 + B + + + 3 / 4 + C + + + 4 / 9 + + Free 2 + 安全:B → A → C,分配 + (b) 中 A、B、C 分别还需要 5、2、3 个,Free 只有 1,任何进程都无法运行完,可能发生死锁,所以拒绝这次申请, + 让 C 等待;(c) 中 B 只差 1 个,运行完归还 4 个后 Free = 5,A 和 C 也能依次完成,所以可以分配。 + diff --git a/tools/diagram/src/deadlock-detect-graph.svg b/tools/diagram/src/deadlock-detect-graph.svg new file mode 100644 index 00000000..23a2c346 --- /dev/null +++ b/tools/diagram/src/deadlock-detect-graph.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + 每种资源只有一个:在资源分配图中找环 + + P1 + + P2 + + P3 + + P4 + + R1 + + R2 + + R3 + + R4 + + + + + + + + + 资源指向进程表示已分配,进程指向资源表示正在请求。红色的边构成环 P1 → R1 → P2 → R2 → P3 → R3 → P1, + 三个进程互相等待,发生死锁;P4 不在环上,但它请求的 R2 被 P2 持有,也会一直等下去。 + diff --git a/tools/diagram/src/deadlock-detect-matrix.svg b/tools/diagram/src/deadlock-detect-matrix.svg new file mode 100644 index 00000000..29a49793 --- /dev/null +++ b/tools/diagram/src/deadlock-detect-matrix.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + 每种资源有多个:逐个找能满足请求的进程 + 资源 R1、R2、R3 + E 总量 + + 5 + + 3 + + 4 + A 剩余 + + 1 + + 1 + + 1 + C 已分配 + P1 + + 1 + + 1 + + 0 + P2 + + 2 + + 0 + + 1 + P3 + + 1 + + 1 + + 2 + R 还在请求 + P1 + + 2 + + 0 + + 1 + P2 + + 1 + + 1 + + 0 + P3 + + 1 + + 2 + + 1 + 按算法依次标记 + + 1. P2 请求 (1 1 0) ≤ A,可以运行 + 结束后归还 (2 0 1),A = (3 1 2) + + 2. P1 请求 (2 0 1) ≤ A,可以运行 + 结束后归还 (1 1 0),A = (4 2 2) + + 3. P3 请求 (1 2 1) ≤ A,可以运行 + 结束后归还 (1 1 2),A = (5 3 4) + 开始时 A 只满足 P2 的请求;P2 结束后归还资源,P1、P3 才依次得到满足。 + 所有进程都能被标记,没有死锁;如果某一步找不到能满足的进程,剩下没被标记的进程就处于死锁中。 + diff --git a/tools/diagram/src/deadlock-safe-state.svg b/tools/diagram/src/deadlock-safe-state.svg new file mode 100644 index 00000000..747bd472 --- /dev/null +++ b/tools/diagram/src/deadlock-safe-state.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + 安全状态:共 10 个资源,存在一个能让所有进程运行完的顺序 + (a) 初始 + A + + + 2 / 7 + B + + + 3 / 5 + C + + + 2 / 4 + + Free 3 + (b) B 再拿 2 个,运行结束 + A + + + 2 / 7 + B + 已完成 + C + + + 2 / 4 + + Free 6 + (c) A 再拿 5 个,运行结束 + A + 已完成 + B + 已完成 + C + + + 2 / 4 + + Free 8 + (d) C 再拿 2 个,运行结束 + A + 已完成 + B + 已完成 + C + 已完成 + + Free 10 + 条形的长度是最大需求,浅蓝部分是已持有的数量。(a) 中 Free = 10 − 2 − 3 − 2 = 3,只够 B 补齐, + B 运行完归还 5 个后 Free = 6,又够 A 补齐 5 个,依此类推。 + 安全不代表一定按这个顺序执行,只说明不管进程怎样申请,系统都能找到一条出路。 + diff --git a/tools/diagram/src/deadlock-transfer.svg b/tools/diagram/src/deadlock-transfer.svg new file mode 100644 index 00000000..2a85d548 --- /dev/null +++ b/tools/diagram/src/deadlock-transfer.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + 死锁:两个线程反向转账 + + T1 + + T2 + + alice + + bob + + 持有 + + 持有 + + 等待 + + 等待 + transfer(alice, bob) + transfer(bob, alice) + 圆形是线程,方形是账户上的锁 + 四个条件同时满足: + + 互斥 + 一把锁同一时刻只能被一个线程持有 + + 占有和等待 + T1 持有 alice 的锁,还在等 bob 的锁 + + 不可抢占 + bob 的锁只能由 T2 自己释放 + + 环路等待 + T1 → bob → T2 → alice → T1 +