1. 引言:哲學家就餐問題與科夫曼(Coffman)死結四條件
1965年,荷蘭計算機科學家艾茲赫爾·迪傑斯特拉(Edsger W. Dijkstra)提出了著名的「哲學家就餐問題(Dining Philosophers Problem)」:五位哲學家 \(P_0 \dots P_4\) 圍坐圓桌,每人左右兩側各有一把叉子 \(F_i\) 與 \(F_{(i+1)\bmod 5}\)(共5把叉子),必須同時拿起左右兩把叉子才能進餐。如果五位哲學家在同一瞬間感到飢餓,並各自拿起左手的叉子 \(F_i\),那麼每個人都會握著左叉永遠等待鄰居釋放右叉——沒有任何硬體故障,整個系統卻陷入了永久停滯,這便是死結(Deadlock)。
形式化地說,行程集合 \(D \subseteq \mathcal{P}\)(\(D \neq \emptyset\))處於死結狀態,若且唯若 \(D\) 中的每一個行程 \(P_i \in D\) 都在等待某個只能由同一集合 \(D\) 中的其他行程 \(P_j \in D\) 觸發的事件(如釋放鎖定資源)。
1971年,Edward G. Coffman Jr. 等人證明了資源死結發生必須同時滿足四個必要條件(科夫曼條件):
- 互斥(Mutual Exclusion):至少有一種資源以排他(非共享)模式被單個行程獨佔。
- 佔有並等待(Hold and Wait):行程在已經持有至少一個資源的同時,阻塞等待取得其他行程持有的額外資源。
- 不可搶佔(No Preemption):已配置給行程的資源不能被作業系統或他者強行剝奪,只能由持有者主動釋放。
- 循環等待(Circular Wait):存在閉合的等待鏈 \(\{P_0, P_1, \dots, P_{k-1}\}\),使得 \(P_i\) 等待 \(P_{(i+1)\bmod k}\) 持有的資源。
由於這四個條件均為必要條件,只要在系統設計中邏輯否定其中任意一個條件,死結在數學上就 \(100\%\) 不可能發生。
2. 資源配置圖(RAG)與環路定理(Holt, 1972)
1972年,Richard C. Holt 引入了有向二分圖——資源配置圖(Resource Allocation Graph, RAG) \(G = (V, E)\),其中頂點集 \(V = \mathcal{P} \cup \mathcal{R}\)(行程頂點 \(P_i\) 與資源類型頂點 \(R_j\),每個 \(R_j\) 含 \(W_j \ge 1\) 個實例),有向邊分為請求邊 \(P_i \to R_j\)(等待配置)與配置邊 \(R_j \to P_i\)(已持有實例)。
關於 RAG 中的有向環(Directed Cycle)與死結的關係,存在三大核心定理:
- 定理 1(無環必無死結):若 RAG 中不存在有向環(即為 DAG),則系統中絕無死結。
- 定理 2(單實例充要定理):若所有資源類型均只有一個實例(\(\forall j, W_j = 1\)),則存在有向環 \(\iff\) 發生死結(充分必要條件)。
- 定理 3(多實例非充分定理):若某資源類型具有多個實例(\(\exists j, W_j > 1\)),則有向環只是死結的必要條件而非充分條件!
flowchart TD
subgraph RAG_Single["情況 A:單實例資源系 (有向環 <==> 必然死結)"]
PA1(("P1 (阻塞)")) -->|"請求"| RA1["R1 (1 個實例: ●)"]
RA1 -->|"已配置"| PA2(("P2 (阻塞)"))
PA2 -->|"請求"| RA2["R2 (1 個實例: ●)"]
RA2 -->|"已配置"| PA1
end
subgraph RAG_Multi["情況 B:多實例資源系 (存在環 P1->R1->P3->R2->P1,但並未死結!)"]
PB1(("P1 (阻塞)")) -->|"請求"| RB1["R1 (2 個實例: ●●)"]
RB1 -->|"配置實例 #1"| PB3(("P3 (阻塞)"))
PB3 -->|"請求"| RB2["R2 (2 個實例: ●●)"]
RB2 -->|"配置實例 #1"| PB1
RB1 -->|"配置實例 #2"| PB2(("P2 (執行中: 無請求邊!)"))
RB2 -->|"配置實例 #2"| PB4(("P4 (執行中: 無請求邊!)"))
end
在情況 B 中,雖然存在環 \(P_1 \to R_1 \to P_3 \to R_2 \to P_1\),但持有第二個實例的 \(P_2\) 與 \(P_4\) 出度為 \(0\)(匯點,正在執行),它們執行完畢釋放 \(R_1, R_2\) 後即可立即解開環路!
3. 策略1(死結預防):全序加鎖規則的反證法證明
在作業系統核心(如 Linux lockdep)與多執行緒工程中,打破第4條件(循環等待)的黃金法則是全域鎖排序(Total Lock Ordering):為所有鎖賦予唯一序號 \(\phi: \mathcal{L} \to \mathbb{N}\),強制要求任何執行緒在持有鎖集 \(\mathcal{H}\) 時,只能請求序號嚴格更大的新鎖:
【反證法證明】:假設系統發生了長度為 \(k \ge 2\) 的循環等待環 \(\{P_0, P_1, \dots, P_{k-1}\}\),其中 \(P_i\) 持有鎖 \(L_i\) 並等待 \(P_{(i+1)\bmod k}\) 持有的鎖 \(L_{(i+1)\bmod k}\)。由於每個執行緒均遵守全序加鎖規則,必有:
\[\phi(L_0) < \phi(L_1) < \phi(L_2) < \cdots < \phi(L_{k-1}) < \phi(L_0) \implies \phi(L_0) < \phi(L_0)\]自然數不可能嚴格小於自身,矛盾(\(\bot\))!因此在全序加鎖規則下(如銀行轉帳始終先鎖 ID 較小的帳戶,或讓哲學家 \(P_4\) 先拿右叉 \(F_0\) 再拿左叉 \(F_4\)),循環等待在數學上絕不可能存在。\(\blacksquare\)
4. 策略2(死結避免):迪傑斯特拉銀行家演算法與 5×3 矩陣推演
當無法靜態固定加鎖順序時,迪傑斯特拉銀行家演算法(Banker’s Algorithm, 1965 / Habermann, 1969)要求行程預先申報最大需求矩陣 \(\mathbf{Max}\),並在每次配置前透過複雜度為 \(O(m \cdot n^2)\) 的安全性演算法檢驗系統是否仍存在至少一條安全序列(Safe Sequence)(注意:\(\text{死結狀態} \subsetneq \text{不安全狀態}\),不安全狀態並非已死結,而是存在未來可能死結的風險)。
設系統有 \(n=5\) 個行程 \(\{P_0 \dots P_4\}\),3 類資源 \(\mathbf{Total} = (A=10, B=5, C=7)\),時刻 \(t_0\) 狀態如下表所示,當前剩餘可用資源為 \(\mathbf{Available} = (10,5,7) - (7,2,5) = \mathbf{(3, 3, 2)}\):
| 行程 | 已配置 \(\mathbf{Allocation}\) \((A,B,C)\) | 最大需求 \(\mathbf{Max}\) \((A,B,C)\) | 剩餘需求 \(\mathbf{Need} = \mathbf{Max} - \mathbf{Allocation}\) | 安全序列推演(初始 \(\mathbf{Work} = (3,3,2)\)) |
|---|---|---|---|---|
| \(P_0\) | \((0, 1, 0)\) | \((7, 5, 3)\) | \((7, 4, 3)\) | 第 4 步滿足:\((7,4,3) \le (7,4,5) \to \mathbf{Work}=(7,5,5)\) |
| \(P_1\) | \((2, 0, 0)\) | \((3, 2, 2)\) | \((1, 2, 2)\) | 第 1 步滿足:\((1,2,2) \le (3,3,2) \to \mathbf{Work}=(5,3,2)\) |
| \(P_2\) | \((3, 0, 2)\) | \((9, 0, 2)\) | \((6, 0, 0)\) | 第 5 步滿足:\((6,0,0) \le (7,5,5) \to \mathbf{Work}=(10,5,7)\) |
| \(P_3\) | \((2, 1, 1)\) | \((2, 2, 2)\) | \((0, 1, 1)\) | 第 2 步滿足:\((0,1,1) \le (5,3,2) \to \mathbf{Work}=(7,4,3)\) |
| \(P_4\) | \((0, 0, 2)\) | \((4, 3, 3)\) | \((4, 3, 1)\) | 第 3 步滿足:\((4,3,1) \le (7,4,3) \to \mathbf{Work}=(7,4,5)\) |
由於存在安全序列 \(\langle P_1, P_3, P_4, P_0, P_2 \rangle\),狀態 \(t_0\) 是安全狀態。若此時 \(P_1\) 請求 \((1,0,2)\),試配置後 \(\mathbf{Available}=(2,3,0)\) 仍滿足 \(\mathbf{Need}_1=(0,2,0)\),立即批准;但若緊接著 \(P_0\) 請求 \((0,2,0)\),試配置後 \(\mathbf{Available}=(2,1,0)\) 將導致所有行程的 \(\mathbf{Need}\) 均無法滿足(進入不安全狀態),銀行家演算法會立即回滾並阻塞 \(P_0\)!
5. 策略3 & 4(偵測恢復與鴕鳥演算法)以及死結 vs. 活結 vs. 飢餓
在關聯式資料庫(RDBMS)中,由於 SQL 語句動態到達,系統採用等待圖(Wait-For Graph, WFG)透過 \(O(|V|+|E|)\) 的 DFS 尋找環路(MySQL InnoDB 阻塞時立即偵測並回滾 Undo 量最小的犧牲者;PostgreSQL 則睡眠等待 deadlock_timeout = 1s 後再偵測)。而在分散式資料庫(如 Google Spanner)中,則採用基於啟動時間戳記 \(\text{TS}(T_i)\)(越小越年長)的 Wait-Die(非搶佔)與 Wound-Wait(搶佔,年長者直接擊傷回滾年輕者,重啟次數更少)協定,且被中止交易重試時必須保留原時間戳記以徹底防止飢餓。至於罕見的作業系統使用者態表項耗盡,UNIX/Windows 則採用鴕鳥演算法(Ostrich Algorithm)直接忽略,以免日常系統呼叫背負沉重的偵測開銷。
flowchart TD
Req["分散式交易 Ti 請求被 Tj 持有的鎖"] --> Cmp{"比較啟動時間戳記: TS(Ti) < TS(Tj) ?<br/>(請求方 Ti 是否比持有方 Tj 更年長?)"}
Cmp -->|"是: Ti 更年長 (Older)"| WD1["【Wait-Die (非搶佔)】: 年長的 Ti 等待年輕的 Tj (老 -> 少)<br/>【Wound-Wait (搶佔)】: 年長的 Ti 擊傷回滾 Tj 並奪鎖!"]
Cmp -->|"否: Ti 更年輕 (Younger)"| WD2["【Wait-Die (非搶佔)】: 年輕的 Ti 自殺中止 (保留原 TS 重試)<br/>【Wound-Wait (搶佔)】: 年輕的 Ti 安靜等待年長的 Tj (少 -> 老)"]
最後,在並行系統的活性故障(Liveness Failures)診斷中,必須嚴格區分死結、活結(Livelock)與飢餓(Starvation):
| 診斷維度 | 死結 (Deadlock) | 活結 (Livelock) | 飢餓 (Starvation) |
|---|---|---|---|
| 核心機制與定義 | 阻塞執行緒形成循環等待鏈,彼此等待對方釋放資源 | 執行緒為避開死結而對稱地不斷執行 trylock 失敗、釋放並立即重試(走廊禮讓死迴圈) | 系統整體正常推進,但特定低優先權執行緒永遠搶不到資源 |
| OS 執行緒狀態與 CPU 佔用 | SLEEPING / BLOCKED(CPU 佔用 \(0\%\)) | RUNNING(CPU 飆升至 \(100\%\) 空轉,有效推進為 0) | 等待佇列中長期停滯(受害者 CPU 為 \(0\%\)) |
| 根本工程解藥 | 全序加鎖(\(\phi(L_1)<\phi(L_2)\))、銀行家演算法、WFG 回滾 | 引入隨機指數退避(Randomized Exponential Backoff with Jitter)打破時間對稱性 | 優先權老化(Aging)、保留原時間戳記、公平 FIFO 佇列鎖(MCS 鎖) |
