在多線程編程中,我們使用互斥鎖(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)

死鎖流程:

  1. 線程 T1 搶到了 鎖 A
  2. 同時,線程 T2 搶到了 鎖 B
  3. T1 想要 鎖 B,但 B 在 T2 手裏,T1 阻塞等待。
  4. 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...
(程序在此處徹底卡死,兩個線程都在等對方放手)

三、 如何避免死鎖?

既然知道了死鎖的原理,避免它就有了方向:

  1. 保持鎖的細粒度:鎖的範圍越小,發生嵌套鎖的可能性越低。
  2. 固定加鎖順序:這是解決雙線程死鎖最有效的方法。如果規定所有線程都必須先拿 A,再拿 B,那麼就不會出現交叉等待了。
  3. 使用 trylock:如果拿不到鎖,不要傻等,先釋放自己手裏的鎖,過一會再重試。

四、 知識小結

知識點

核心説明

難度係數

死鎖定義

是一種由錯誤操作導致的狀態,而非鎖的類型。

★★☆☆☆

死鎖條件1

反覆 Lock:線程對自己持有的鎖再次加鎖。

★★★★☆

死鎖條件2

循環等待:T1 有 A 想要 B,T2 有 B 想要 A。

★★★★★

實驗意義

只有親手寫出死鎖,才能在未來的調試中快速識別它。

★★★★☆

Linux 多線程避坑:什麼是死鎖?手把手教你復現兩種經典死鎖_#include