各位讀者大家好,承接上一回關於線程死鎖的探討,本文將繼續深入分析死鎖產生的第二種核心原因,並將先前的偽代碼邏輯完整轉化為可編譯運行的 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 的加鎖順序差異:
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);
}
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 // 程序卡死,資源無法釋放
mutex_value1,開始讀取數據。mutex_value2,開始寫入數據。mutex_value2 → 阻塞(被 T2 持有)。mutex_value1 → 阻塞(被 T1 持有)。pthread_mutex_destroy 失敗報錯。這段代碼是為了演示死鎖而故意編寫的「反面教材」,實際開發中絕不可這樣寫。
避免此類死鎖的黃金法則:
pthread_mutex_trylock:嘗試非阻塞加鎖,失敗即返回錯誤碼,避免線程掛起。pthread_mutex_timedlock 設置等待上限。完整的可編譯項目代碼(包含 read_data/write_data 實現、主函數線程創建邏輯等)已上傳至資源區,讀者可下載自行編譯調試,觀察死鎖發生的完整過程。
關於線程死鎖的實戰剖析暫告一段落。下一期我們將探討如何使用條件變量實現更高效的線程協作,敬請期待。