說明:本文由【2,3】整理而得。
這篇文章主要從一個 linux 下一個pthread_cancel 函數引起的多線程死鎖小例子出發來說明Linux 系統對 POSIX線程取消點的實現方式,以及如何避免因此產生的線程死鎖。
目 錄:
1. 一個 pthread_cancel 引起的線程死鎖小例子
2. 取消點(Cancellation Point)
3. 取消類型(Cancellation Type)
4. Linux 的取消點實現
5. 對示例函數進入死鎖的解釋
6. 如何避免因此產生的死鎖
7. 結論
8. 參考文獻
1. 一個 pthread_cancel 引起的線程死鎖小例子
下面是一段在Linux 平臺下能引起線程死鎖的小例子。這個實例程序僅僅是使用了條件變量和互斥量進行一個簡單的線程同步,thread0首先啟動,鎖住互斥量 mutex,然后調用pthread_cond_wait,它將線程tid[0] 放在等待條件的線程列表上后,對mutex 解鎖。thread1啟動后等待 10 秒鐘,此時pthread_cond_wait 應該已經將mutex 解鎖,這時 tid[1]線程鎖住 mutex,然后廣播信號喚醒cond 等待條件的所有等待線程,之后解鎖 mutex。當 mutex解鎖后,tid[0] 線程的pthread_cond_wait 函數重新鎖住mutex 并返回,最后 tid[0]再對 mutex 進行解鎖。
示例代碼
#include <pthread.h>#include "stdio.h"#include "stdlib.h"#include "unistd.h"pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void* thread0(void* arg){ pthread_mutex_lock(&mutex); PRintf("in thread 0 tag 1/n"); pthread_cond_wait(&cond, &mutex); printf("in thread 0 tag 2/n"); pthread_mutex_unlock(&mutex); printf("in thread 0 tag 3/n"); pthread_exit(NULL);}void* thread1(void* arg){ sleep(10); printf("in thread 1 tag 1/n"); pthread_mutex_lock(&mutex); printf("in thread 1 tag 2/n"); pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mutex); printf("in thread 1 tag 3/n"); pthread_exit(NULL);}int main(){ pthread_t tid[2]; if (pthread_create(&tid[0], NULL, thread0, NULL) !=0) { exit(1); } if (pthread_create(&tid[1], NULL, thread1, NULL) !=0) { exit(1); } sleep(5); printf("in main thread tag 1/n"); pthread_cancel(tid[0]); pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return0;}示例代碼_對上述程序的跟蹤
[Thread debugging using libthread_db enabled]Breakpoint 8, main () at testthread.cpp:3434if (pthread_create(&tid[0], NULL, thread0, NULL) !=0) (gdb) bt#0 main () at testthread.cpp:34(gdb) n[New Thread 0xb7fecb70 (LWP 2494)]in thread 0 tag 1Breakpoint 9, main () at testthread.cpp:3838if (pthread_create(&tid[1], NULL, thread1, NULL) !=0) (gdb) bt#0 main () at testthread.cpp:38(gdb) n[Switching to Thread 0xb7fecb70 (LWP 2494)]Breakpoint 1, thread0 (arg=0x0) at testthread.cpp:1313 pthread_cond_wait(&cond, &mutex);(gdb) n[New Thread 0xb77ebb70 (LWP 2495)]in main thread tag 1[Switching to Thread 0xb7fee6d0 (LWP 2491)]Breakpoint 10, main () at testthread.cpp:4444 pthread_cancel(tid[0]);(gdb) nin thread 1 tag 1Breakpoint 11, main () at testthread.cpp:4646 pthread_join(tid[0], NULL);(gdb) n[Switching to Thread 0xb77ebb70 (LWP 2495)]Breakpoint 2, thread1 (arg=0x0) at testthread.cpp:2424 pthread_mutex_lock(&mutex);(gdb) n[Thread 0xb7fecb70 (LWP 2494) exited][Switching to Thread 0xb7fee6d0 (LWP 2491)]Breakpoint 12, main () at testthread.cpp:4747 pthread_join(tid[1], NULL);(gdb) n^CProgram received signal SIGINT, Interrupt.0x00110416in __kernel_vsyscall ()(gdb) info breakNum Type Disp Enb Address What1 breakpoint keep y 0x08048742in thread0(void*) at testthread.cpp:13 breakpoint already hit 1 time2 breakpoint keep y 0x080487a4in thread1(void*) at testthread.cpp:24 breakpoint already hit 1 time3 breakpoint keep y 0x08048762in thread0(void*) at testthread.cpp:154 breakpoint keep y 0x0804877ain thread0(void*) at testthread.cpp:175 breakpoint keep y 0x080487bcin thread1(void*) at testthread.cpp:266 breakpoint keep y 0x080487c8in thread1(void*) at testthread.cpp:277 breakpoint keep y 0x080487e0in thread1(void*) at testthread.cpp:298 breakpoint keep y 0x080487f5in main() at testthread.cpp:34 breakpoint already hit 1 time9 breakpoint keep y 0x0804882ein main() at testthread.cpp:38 breakpoint already hit 1 time10 breakpoint keep y 0x08048882in main() at testthread.cpp:44 breakpoint already hit 1 time---Type <return> to continue, or q <return> to quit---11 breakpoint keep y 0x0804888ein main() at testthread.cpp:46 breakpoint already hit 1 time12 breakpoint keep y 0x080488a2in main() at testthread.cpp:47 breakpoint already hit 1 time13 breakpoint keep y 0x080488b6in main() at testthread.cpp:49(gdb)我們發現,
Breakpoint 12, main () at testthread.cpp:47
47 pthread_join(tid[1], NULL);
(gdb) n
^C
一直卡在這里。
看起來似乎沒有什么問題,但是 main 函數調用了一個pthread_cancel 來取消 tid[0] 線程。上面程序編譯后運行時會發生無法終止情況,看起來像是pthread_cancel 將 tid[0] 取消時沒有執行 pthread_mutex_unlock函數,這樣 mutex 就被永遠鎖住,線程tid[1] 也陷入無休止的等待中。事實是這樣嗎?
2. 取消點(Cancellation Point)
要注意的是 pthread_cancel調用并不等待線程終止,它只提出請求。線程在取消請求(pthread_cancel)發出后會繼續運行,直到到達某個取消點(Cancellation Point)。取消點是線程檢查是否被取消并按照請求進行動作的一個位置。pthread_cancel manual 說以下幾個 POSIX 線程函數是取消點:
pthread_join(3)
pthread_cond_wait(3)
pthread_cond_timedwait(3)
pthread_testcancel(3)
sem_wait(3)
sigwait(3)
以及read()、write()等會引起阻塞的系統調用都是Cancelation-point,而其他pthread函數都不會引起Cancelation動作。
在中間我們可以找到 pthread_cond_wait 就是取消點之一。
但是,令人迷惑不解的是,所有介紹 Cancellation Points的文章都僅僅說,當線程被取消后,將繼續運行到取消點并發生取消動作。但我們注意到上面例子中 pthread_cancel前面 main 函數已經sleep 了 5秒,那么在 pthread_cancel 被調用時,thread0 到底運行到pthread_cond_wait 沒有?
如果 thread0 運行到了pthread_cond_wait,那么照上面的說法,它應該繼續運行到下一個取消點并發生取消動作,而后面并沒有取消點,所以thread0 應該運行到 pthread_exit并結束,這時 mutex 就會被解鎖,這樣就不應該發生死鎖啊。
說明:
從我的GDB中可以看出,運行到pthread_cond_wait這里后,就沒有往下運行了。應該說,這是當前的取消點。
3. 取消類型(Cancellation Type)
我們會發現,通常的說法:某某函數是 Cancellation Points,這種方法是容易令人混淆的。因為函數的執行是一個時間過程,而不是一個時間點。其實真正的Cancellation Points 只是在這些函數中Cancellation Type 被修改為PHREAD_CANCEL_ASYNCHRONOUS 和修改回PTHREAD_CANCEL_DEFERRED 中間的一段時間。
POSIX 的取消類型有兩種,一種是延遲取消(PTHREAD_CANCEL_DEFERRED),這是系統默認的取消類型,即在線程到達取消點之前,不會出現真正的取消;另外一種是異步取消(PHREAD_CANCEL_ASYNCHRONOUS),使用異步取消時,線程可以在任意時間取消。
4. Linux 的取消點實現
下面我們看 Linux 是如何實現取消點的。(其實這個準確點兒應該說是GNU 取消點實現,因為 pthread庫是實現在 glibc 中的。)我們現在在 Linux 下使用的pthread 庫其實被替換成了 NPTL,被包含在 glibc庫中。
以 pthread_cond_wait 為例,glibc-2.6/nptl/pthread_cond_wait.c中:
示例代碼
145/* Enable asynchronous cancellation. Required by the standard. */146 cbuffer.oldtype = __pthread_enable_asynccancel ();147148/* Wait until woken by signal or broadcast. */149 lll_futex_wait (&cond->__data.__futex, futex_val);150151/* Disable asynchronous cancellation. */152 __pthread_disable_asynccancel (cbuffer.oldtype);我們可以看到,在線程進入等待之前,pthread_cond_wait先將線程取消類型設置為異步取消(__pthread_enable_asynccancel),當線程被喚醒時,線程取消類型被修改回延遲取消__pthread_disable_asynccancel 。
這就意味著,所有在 __pthread_enable_asynccancel之前接收到的取消請求都會等待__pthread_enable_asynccancel執行之后進行處理,所有在__pthread_disable_asynccancel之前接收到的請求都會在__pthread_disable_asynccancel 之前被處理,所以真正的Cancellation Point 是在這兩點之間的一段時間。(也就是在__pthread_enable_asynccancel與__pthread_disable_asynccancel間處理取消請求)
5. 對示例函數進入死鎖的解釋
當main函數中調用pthread_cancel 前,thread0已經進入了 pthread_cond_wait函數并將自己列入等待條件的線程列表中(lll_futex_wait)。這個可以通過GDB 在各個函數上設置斷點來驗證。
當 pthread_cancel 被調用時,tid[0]線程仍在等待,取消請求發生在 __pthread_disable_asynccancel前,所以會被立即響應。但是 pthread_cond_wait為注冊了一個線程清理程序(glibc-2.6/nptl/pthread_cond_wait.c):
126 /* Before we block we enable cancellation. Therefore we have to
127 install a cancellation handler. */
128 __pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer);
那么這個線程 清理程序 __condvar_cleanup 干了什么事情呢?我們可以注意到在它的實現最后(glibc-2.6/nptl/pthread_cond_wait.c):
85 /* Get the mutex before returning unless asynchronous cancellation
86 is in effect. */
87 __pthread_mutex_cond_lock (cbuffer->mutex);
88}
哦,__condvar_cleanup 在最后將mutex 重新鎖上了。而這時候 thread1 還在休眠(sleep(10)),等它醒來時,mutex將會永遠被鎖住,這就是為什么 thread1陷入無休止的阻塞中。
【可是為什么pthread_cond_wait要在最后上鎖呢?】
6. 如何避免因此產生的死鎖
由于線程清理函數 pthread_cleanup_push 使用的策略是先進后出(FILO),那么我們可以在pthread_cond_wait 函數前先注冊一個線程處理函數:
示例代碼
void cleanup(void*arg){ pthread_mutex_unlock(&mutex);}void* thread0(void* arg){ pthread_cleanup_push(cleanup, NULL); // thread cleanup handler pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); pthread_cleanup_pop(0); pthread_exit(NULL);}這樣,當線程被取消時,先執行 pthread_cond_wait 中注冊的線程清理函數 __condvar_cleanup,將mutex 鎖上,再執行 thread0中注冊的線程處理函數 cleanup,將mutex解鎖。這樣就避免了死鎖的發生。
7. 結論
多線程下的線程同步一直是一個讓人很頭痛的問題。POSIX 為了避免立即取消程序引起的資源占用問題而引入的 Cancellation Points概念是一個非常好的設計,但是不合適的使用 pthread_cancel仍然會引起線程同步的問題。了解POSIX 線程取消點在 Linux 下的實現更有助于理解它的機制和有利于更好的應用這個機制。
8. 參考文獻
[1] W. Richard Stevens, Stephen A. Rago: Advanced Programming in the UNIX Environment, 2nd Edition.
[2] Linux Manpage
http://wzw19191.blog.163.com/blog/static/131135470200992610550684/
[3] http://hi.baidu.com/hackers365/blog/item/412d0f085c1fd18f0a7b8205.html
【4】http://blog.csdn.net/yanook/article/details/6589798
新聞熱點
疑難解答