在多線程編程中,我們使用互斥鎖(Mutex)來解決數據混亂問題。但如果鎖用得不對,就會引發一種更嚴重的後果——死鎖(Deadlock)。
首先要糾正一個概念:死鎖不是一種鎖的類型,而是由於錯誤使用鎖,導致程序陷入的一種永久阻塞(卡死)的狀態。
今天我們通過代碼,現場還原兩種最常見的死鎖場景。
一、 第一種死鎖:作繭自縛 (Repeated Locking)
這是最簡單、也最容易犯的錯誤:同一個線程,對同一把鎖,連續加鎖兩次。
1. 原理解析
回顧互斥鎖的本質,它就像一個初值為 1 的計數器:
- 第 1 次
lock():值從 1 變為 0,成功拿到鎖。 - 第 2 次
lock():值已經是 0 了,線程進入阻塞狀態,等待鎖變回 1。
關鍵點:鎖想要變回 1,必須執行 unlock()。但持有鎖的線程(就是你自己)正在第二步那裏阻塞着,永遠無法向下執行去調用 unlock()。於是,線程自己把自己鎖死了。
2. 代碼復現 (deadlock_self.c)
#include <stdio.h>
#include <pthread.h>
// 靜態初始化互斥鎖
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main() {
printf(">>> [Main] 嘗試第一次加鎖...\n");
pthread_mutex_lock(&mutex);
printf(">>> [Main] 第一次加鎖成功!\n");
// 典型場景:
// 1. 代碼邏輯太複雜,忘記自己已經拿過鎖了
// 2. 調用了一個子函數,子函數裏也加了這把鎖
printf(">>> [Main] 嘗試第二次加鎖(即將在死鎖中永生)...\n");
pthread_mutex_lock(&mutex);
// 這行代碼永遠不會執行
printf(">>> [Main] 第二次加鎖成功(這行永遠看不見)\n");
pthread_mutex_unlock(&mutex);
return 0;
}
3. 運行結果
>>> [Main] 嘗試第一次加鎖...
>>> [Main] 第一次加鎖成功!
>>> [Main] 嘗試第二次加鎖(即將在死鎖中永生)...
(程序在此處永久卡死,光標閃爍,不再有輸出)
二、 第二種死鎖:互相傷害 (Circular Wait)
這通常發生在兩個線程和兩個鎖的場景中。簡單來説就是:我拿着你的,你拿着我的,咱倆還都互不相讓。
1. 原理解析
假設有:
- 資源 A(配鎖 mutex_A)
- 資源 B(配鎖 mutex_B)
死鎖流程:
- 線程 T1 搶到了 鎖 A。
- 同時,線程 T2 搶到了 鎖 B。
- T1 想要 鎖 B,但 B 在 T2 手裏,T1 阻塞等待。
- T2 想要 鎖 A,但 A 在 T1 手裏,T2 阻塞等待。
結果:形成了一個環形等待鏈(T1等T2,T2等T1),誰也動不了。
2. 代碼復現 (deadlock_mutual.c)
為了確保死鎖發生,我們在兩個線程獲取第一把鎖後,都加上 sleep(1),給對方留出時間去獲取另一把鎖。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER;
void *tfn1(void *arg) {
// 1. 拿鎖 A
pthread_mutex_lock(&mutex_A);
printf("--- [線程A] 拿到鎖 A\n");
// 模擬處理時間,給線程B機會去拿鎖B
sleep(1);
printf("--- [線程A] 嘗試拿鎖 B...\n");
// 2. 拿鎖 B (此時鎖B應該在線程B手裏)
pthread_mutex_lock(&mutex_B);
printf("--- [線程A] 拿到鎖 B,任務完成\n");
pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
return NULL;
}
void *tfn2(void *arg) {
// 1. 拿鎖 B
pthread_mutex_lock(&mutex_B);
printf("--- [線程B] 拿到鎖 B\n");
sleep(1);
printf("--- [線程B] 嘗試拿鎖 A...\n");
// 2. 拿鎖 A (此時鎖A應該在線程A手裏)
pthread_mutex_lock(&mutex_A);
printf("--- [線程B] 拿到鎖 A,任務完成\n");
pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, tfn1, NULL);
pthread_create(&t2, NULL, tfn2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
3. 運行結果
--- [線程A] 拿到鎖 A
--- [線程B] 拿到鎖 B
--- [線程A] 嘗試拿鎖 B...
--- [線程B] 嘗試拿鎖 A...
(程序在此處徹底卡死,兩個線程都在等對方放手)
三、 如何避免死鎖?
既然知道了死鎖的原理,避免它就有了方向:
- 保持鎖的細粒度:鎖的範圍越小,發生嵌套鎖的可能性越低。
- 固定加鎖順序:這是解決雙線程死鎖最有效的方法。如果規定所有線程都必須先拿 A,再拿 B,那麼就不會出現交叉等待了。
- 使用 trylock:如果拿不到鎖,不要傻等,先釋放自己手裏的鎖,過一會再重試。
四、 知識小結
|
知識點
|
核心説明
|
難度係數
|
|
死鎖定義 |
是一種由錯誤操作導致的狀態,而非鎖的類型。 |
★★☆☆☆
|
|
死鎖條件1 |
反覆 Lock:線程對自己持有的鎖再次加鎖。 |
★★★★☆
|
|
死鎖條件2 |
循環等待:T1 有 A 想要 B,T2 有 B 想要 A。 |
★★★★★
|
|
實驗意義 |
只有親手寫出死鎖,才能在未來的調試中快速識別它。
|
★★★★☆
|