首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C語言實戰:深入剖析線程死鎖的第二種成因與代碼實現

     編輯:關於C語言

前言

各位讀者大家好,承接上一回關於線程死鎖的探討,本文將繼續深入分析死鎖產生的第二種核心原因,並將先前的偽代碼邏輯完整轉化為可編譯運行的 C 語言實戰代碼。閒話少說,讓我們直接進入技術核心。

死鎖的第二種成因:鎖獲取順序不一致

上一回我們介紹了同一線程重複加鎖導致的死鎖。這一回要講的經典場景是多線程按不同順序請求同一組鎖。當線程 1 持有鎖 A 並等待鎖 B,而線程 2 持有鎖 B 並等待鎖 A 時,雙方陷入永久阻塞,這就是教科書級別的「循環等待」死鎖。

代碼架構與關鍵數據結構

為了聚焦於死鎖邏輯本身,我們沿用前文示例中的互斥量基礎框架,定義兩個全局互斥量供線程競爭:

#if MUTEX_ENABLE
pthread_mutex_t mutex_value1;
pthread_mutex_t mutex_value2;
#endif

初始化與銷毀:資源管理的規範動作

在 main 函數中,必須成對完成初始化與銷毀,這是編寫健壯多線程程序的基本素養:

// 初始化
res = pthread_mutex_init(&mutex_value1, NULL);
res = pthread_mutex_init(&mutex_value2, NULL);

// ... 線程創建與等待邏輯 ...

// 銷毀
#if MUTEX_ENABLE
res = pthread_mutex_destroy(&mutex_value1);
res = pthread_mutex_destroy(&mutex_value2);
#endif

核心邏輯:兩個線程的「錯位」加鎖

以下是導致死鎖的核心代碼。請仔細觀察 thread_func1 與 thread_func2 對 mutex_value1 和 mutex_value2 的加鎖順序差異:

線程 1:先鎖 1,再鎖 2

void *thread_func1(void *param) {
    int i = 0, res = 0;
    pthread_t thread_id = pthread_self();
    printf("Thread ID::%u -----------S---------- \n", (unsigned int)thread_id);

    while (i++ < 4) {
        #if MUTEX_ENABLE
        res = pthread_mutex_lock(&mutex_value1); // 先獲取鎖 1
        if (res != 0) printf(" mutex1 lock failed \n");
        #endif

        read_data("Thread_1"); // 模擬讀取操作

        #if MUTEX_ENABLE
        res = pthread_mutex_lock(&mutex_value2); // 再嘗試獲取鎖 2
        if (res != 0) printf(" mutex2 lock failed \n");
        #endif

        // 解鎖順序與加鎖順序相反(良好習慣)
        #if MUTEX_ENABLE
        res = pthread_mutex_unlock(&mutex_value2);
        if (res != 0) printf(" mutex2 unlock failed \n");
        res = pthread_mutex_unlock(&mutex_value1);
        if (res != 0) printf(" mutex1 unlock failed \n");
        #endif

        sleep(2);
    }
    printf("Thread ID::%u -----------E---------- \n", (unsigned int)thread_id);
    pthread_exit(&status);
}

線程 2:先鎖 2,再鎖 1

void *thread_func2(void *param) {
    int i = 0, res = 0;
    pthread_t thread_id = pthread_self();
    printf("Thread ID::%u -----------S---------- \n", (unsigned int)thread_id);

    while (i++ < 4) {
        #if MUTEX_ENABLE
        res = pthread_mutex_lock(&mutex_value2); // 先獲取鎖 2
        if (res != 0) printf(" mutex2 lock failed \n");
        #endif

        write_data("Thread_2"); // 模擬寫入操作

        #if MUTEX_ENABLE
        res = pthread_mutex_lock(&mutex_value1); // 再嘗試獲取鎖 1
        if (res != 0) printf(" mutex1 lock failed \n");
        #endif

        #if MUTEX_ENABLE
        res = pthread_mutex_unlock(&mutex_value1);
        if (res != 0) printf(" mutex1 unlock failed \n");
        res = pthread_mutex_unlock(&mutex_value2);
        if (res != 0) printf(" mutex2 unlock failed \n");
        #endif

        sleep(1);
    }
    printf("Thread ID::%u -----------E---------- \n", (unsigned int)thread_id);
    pthread_exit(&status);
}

運行結果與現象分析

編譯運行後,典型的輸出如下:

Create first thread
Create second thread
Thread ID::3076344640 -----------S----------
[Thread_1] start reading data
Thread ID::3067951936 -----------S----------
[Thread_2] start writing data
[Thread_1] data = 0
[Thread_1] end reading data
[Thread_2] data = 1
[Thread_2] end writing data
mutex2 can't be destroyed  // 程序卡死,資源無法釋放

死鎖形成的時序圖解

  1. T1 鎖定 mutex_value1,開始讀取數據。
  2. T2 鎖定 mutex_value2,開始寫入數據。
  3. T1 讀取完畢,嘗試鎖定 mutex_value2 → 阻塞(被 T2 持有)。
  4. T2 寫入完畢,嘗試鎖定 mutex_value1 → 阻塞(被 T1 持有)。
  5. 雙方無限等待,進程凍結,主線程調用 pthread_mutex_destroy 失敗報錯。

核心教訓與避免策略

這段代碼是為了演示死鎖而故意編寫的「反面教材」,實際開發中絕不可這樣寫。

避免此類死鎖的黃金法則:

  • 統一加鎖順序:全程序規定固定的鎖獲取順序(如總是先鎖 1 再鎖 2),從根源上打破「循環等待」條件。
  • 使用 pthread_mutex_trylock:嘗試非阻塞加鎖,失敗即返回錯誤碼,避免線程掛起。
  • 鎖超時機制:結合 pthread_mutex_timedlock 設置等待上限。
  • 減少鎖持有範圍:將臨界區縮小到最必要的代碼段。

完整代碼獲取

完整的可編譯項目代碼(包含 read_data/write_data 實現、主函數線程創建邏輯等)已上傳至資源區,讀者可下載自行編譯調試,觀察死鎖發生的完整過程。


關於線程死鎖的實戰剖析暫告一段落。下一期我們將探討如何使用條件變量實現更高效的線程協作,敬請期待。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved