使用 pthread_cancel 引入的死鎖問題


先來說一下 pthread_cancel 基本概念。

pthread_cancel 調用並不是強制終止線程,它只提出請求。
線程如何處理 cancel 信號則由目標線程自己決定,可以是忽略、可以是立即終止、或者繼續運行至 Cancelation-point(取消點),由不同的 Cancelation 狀態決定。

有幾個與 pthread_cancel  相關的函數也要提及一下:

int pthread_setcancelstate(int state, int *oldstate)

設置本線程對 cancel 信號的處理,state 有兩種值:PTHREAD_CANCEL_ENABLE(缺省)和 PTHREAD_CANCEL_DISABLE,分別表示收到信號后設為 CANCLED 狀態和忽略 CANCEL 信號繼續運行。

int pthread_setcanceltype(int type, int *oldtype)

設置本線程取消動作的執行時機,type 有兩種取值:PTHREAD_CANCEL_DEFFERED 和 PTHREAD_CANCEL_ASYCHRONOUS,僅當 cancel 狀態為 ENABLE 時有效,分別表示收到信號后繼續運行至下一個取消點再退出和立即執行取消動作(退出)。

void pthread_testcancel(void)

當線程中不包含取消點,但是又需要取消點的地方需使用此函數創建一個取消點,以便在一個沒有包含取消點的執行代碼線程中響應取消請求.

當然,線程的取消點並非只有調用該函數來設定,系統中有些函數調用也具有取消點特性如:pthread_cond_wait,sigwait(2) 等等。具體的大家可以網絡上查詢。

 

了解了 pthread_cancel 的取消機制之后,進入 bug 分析環節。

源碼如下:

 1 #include <pthread.h>
 2 #include "stdio.h"
 3 #include "stdlib.h"
 4 #include "unistd.h"
 5 
 6 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  7 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  8 
 9 void* testThreadOne(void* arg) 10 { 11   pthread_mutex_lock(&mutex); 12   puts("ThreadOne label 1."); 13   pthread_cond_wait(&cond, &mutex); 14   puts("ThreadOne label 2."); 15   pthread_mutex_unlock(&mutex); 16   puts("ThreadOne label 3."); 17   pthread_exit(NULL); 18 } 19 
20 void* testThreadTwo(void* arg) 21 { 22   sleep(2); 23   puts("ThreadTwo label 1."); 24   pthread_mutex_lock(&mutex); 25   puts("ThreadTwo label 2."); 26   pthread_cond_broadcast(&cond); 27   pthread_mutex_unlock(&mutex); 28   puts("ThreadTwo label 3."); 29   pthread_exit(NULL); 30 } 31 
32 int main() 33 { 34   pthread_t tid[2] = {0}; 35 
36   pthread_create(&tid[0], NULL, testThreadOne, NULL); 37   pthread_create(&tid[1], NULL, testThreadTwo, NULL); 38 
39   sleep(1); 40   puts("Main thread label 1."); 41   pthread_cancel(tid[0]); 42 
43   pthread_join(tid[0], NULL); 44   pthread_join(tid[1], NULL); 45   pthread_mutex_destroy(&mutex); 46   pthread_cond_destroy(&cond); 47 
48   return 0; 49 }

先來編譯運行此程序:

ThreadOne label 1. Main thread label 1. ThreadTwo label 1.

結果不盡人意,程序並沒有退出,產生了死鎖的問題。

結合打印我們可以分析出程序停在了線程二 pthread_mutex_lock(&mutex) 的位置。

 

我們可以大致的梳理下整個程序的運行流程:

兩個線程創建后,主線程會睡眠1秒,由於線程二開始也是要睡眠,所以此時線程一取得了運行權,
它會先將 mutex 上鎖,並輸出 label 1 信息,wait 函數內部會先將 mutex 解鎖,然后等待 cond 條件,暫時沒有其它線程喚醒,所以線程一會阻塞在此處。

由於主線程的睡眠時間較短,所以會優先被喚醒繼續執行,輸出 main label 1,隨后調用 pthread_cancel 函數向線程一發出退出請求,並阻塞在 join 處。
此時線程一的 cancel 請求處理處於“受理”的狀態,並且恰巧處於請求點(wait 調用),所以線程一會正常的退出。

流程繼續,線程二的睡眠時間到並取得了運行權,先是輸出 label 1 信息,然后請求 lock mutex,問題來了,線程二會在此阻塞下去。主線程也阻塞在 join 處無法退出。原因是為什么呢?

仔細一想我們就可以得出答案,通過之前的知識儲備,wait 在調用時其內部會先將 mutex 解鎖,如果被條件喚醒的話,它的內部會再次將 mutex 上鎖來占據資源。

其實我們通過查看 GLIBC 的源碼就可以來證明一切,我在這里貼出 2.30 版本的部分源碼:

 1 static __always_inline int
 2 __pthread_cond_wait_common (pthread_cond_t *cond, pthread_mutex_t *mutex,
 3   clockid_t clockid,
 4   const struct timespec *abstime)
 5 {
 6     ...
 7   err = __pthread_mutex_unlock_usercnt (mutex, 0);
 8     ...
 9   futex_wait_cancelable(cond->__data.__g_signals + g, 0, private)
10     --> oldtype = __pthread_enable_asynccancel ();
11     --> int err = lll_futex_timed_wait (futex_word, expected, NULL, private);
12     --> __pthread_disable_asynccancel (oldtype);
13     ...
14   err = __pthread_mutex_cond_lock (mutex);
15     ...
16 }

通過源碼可知,wait 函數的入口和出口處分別會對 mutex 進行加鎖和解鎖的操作,而在 __pthread_enable_asynccancel () 與 __pthread_disable_asynccancel (oldtype) 之間的時段里就對應着我們前面提到過的取消點,只有程序執行在兩個函數之前時才可以被 cancel(默認狀態下) 函數所取消。而我們使用 cancel 請求處於取消點的 wait 函數退出時,線程不是直接退出,而是將 wait 函數執行完成,所以BUG就這樣引入了,mutex 並沒有得到釋放,可我們一定要這樣的使用 cancel 函數的話,就沒有解決鎖釋放的方法了么?

答案是有的,官方早已想到了這點,為我們精心准備了 pthread_cleanup_push 函數,它的作用就是在一些情況下退出線程做出一些收尾的動作,如使用 phread_exit、pthread_cancel 函數退出線程,在網上有說過線程異常退出的也可以調用 clean 函數,可筆者嘗試過內存越界訪問情況的異常,clean 函數卻沒有被調用,可能是指的不是這種情況吧。

利用 clean 函數,我們可以對前面的源程序中的線程一做一些修改,如下所示:

 1 void cleanup(void *arg)  2 {  3     pthread_mutex_unlock(&mutex);  4 }  5 
 6 void *testThreadOne(void *arg)  7 {  8  pthread_cleanup_push(cleanup, NULL);  9     pthread_mutex_lock(&mutex); 10     puts("ThreadOne label 1."); 11     pthread_cond_wait(&cond, &mutex); 12     puts("ThreadOne label 2."); 13     pthread_mutex_unlock(&mutex); 14     puts("ThreadOne label 3."); 15     pthread_cleanup_pop(0); 16  pthread_exit(NULL); 17 }

再次編譯執行程序:

ThreadOne label 1. Main thread label 1. ThreadTwo label 1. ThreadTwo label 2. ThreadTwo label 3.

Perfect! mutex 得到了正確的釋放,程序正常執行完畢。

 

本文參考自:https://www.cnblogs.com/mydomain/archive/2011/08/15/2139830.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM