很不錯的一篇文章,對POSIX的線程的取消點(Cancellation Point)的概念和實現方式做了深入的解析, ZZ一下。
以下ZZ自:http://blog.solrex.cn/articles/linux-implementation-of-posix-thread-cancellation-points.html
摘要:
這篇文章主要從一個 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
進行解鎖。
[code:c]
1 #include <pthread.h>
2
3 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
4 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
5
6 void* thread0(void* arg)
7 {
8 pthread_mutex_lock(&mutex);
9 pthread_cond_wait(&cond, &mutex);
10 pthread_mutex_unlock(&mutex);
11 pthread_exit(NULL);
12 }
13
14 void* thread1(void* arg)
15 {
16 sleep(10);
17 pthread_mutex_lock(&mutex);
18 pthread_cond_broadcast(&cond);
19 pthread_mutex_unlock(&mutex);
20 pthread_exit(NULL);
21 }
22 int main()
23 {
24 pthread_t tid[2];
25 if (pthread_create(&tid[0], NULL, &thread0, NULL) != 0) {
26 exit(1);
27 }
28 if (pthread_create(&tid[1], NULL, &thread1, NULL) != 0) {
29 exit(1);
30 }
31 sleep(5);
32 pthread_cancel(tid[0]);
33
34 pthread_join(tid[0], NULL);
35 pthread_join(tid[1], NULL);
36
37 pthread_mutex_destroy(&mutex);
38 pthread_cond_destroy(&cond);
39 return 0;
40 }[/code]
看起來似乎沒有什麼問題,但是 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 線程函數是取消點:
[code]
pthread_join(3)
pthread_cond_wait(3)
pthread_cond_timedwait(3)
pthread_testcancel(3)
sem_wait(3)
sigwait(3)
[/code]
在中間我們可以找到 pthread_cond_wait 就是取消點之一。
但是,令人迷惑不解的是,所有介紹 Cancellation Points
的文章都僅僅說,當線程被取消後,將繼續運行到取消點並發生取消動作。但我們注意到上面例子中 pthread_cancel 前面 main
函數已經 sleep 了 5 秒,那麼在 pthread_cancel 被調用時,thread0 到底運行到
pthread_cond_wait 沒有?
如果 thread0 運行到了
pthread_cond_wait,那麼照上面的說法,它應該繼續運行到下一個取消點並發生取消動作,而後面並沒有取消點,所以 thread0
應該運行到 pthread_exit 並結束,這時 mutex 就會被解鎖,這樣就不應該發生死結啊。
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 中:
[code:c]
145 /* Enable asynchronous cancellation. Required by the standard. */
146 cbuffer.oldtype = __pthread_enable_asynccancel ();
147
148 /* Wait until woken by signal or broadcast. */
149 lll_futex_wait (&cond->__data.__futex, futex_val);
150
151 /* Disable asynchronous cancellation. */
152 __pthread_disable_asynccancel (cbuffer.oldtype);[/code]
我們可以看到,線上程進入等待之前,pthread_cond_wait
先將線程取消類型設定為非同步取消(__pthread_enable_asynccancel),當線程被喚醒時,線程取消類型被修改回延遲取消
__pthread_disable_asynccancel 。
這就意味著,所有在 __pthread_enable_asynccancel 之前接收到的取消請求都會等待
__pthread_enable_asynccancel 執行之後進行處理,所有在 __pthread_disable_asynccancel
之前接收到的請求都會在 __pthread_disable_asynccancel 之前被處理,所以真正的 Cancellation
Point 是在這兩點之間的一段時間。
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):
[code: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);[/code]
那麼這個線程清理程式 __condvar_cleanup 幹了什麼事情呢?我們可以注意到在它的實現最後(glibc-2.6/nptl/pthread_cond_wait.c):
[code:c]
85 /* Get the mutex before returning unless asynchronous cancellation
86 is in effect. */
87 __pthread_mutex_cond_lock (cbuffer->mutex);
88}[/code]
哦,__condvar_cleanup 在最後將 mutex 重新鎖上了。而這時候 thread1 還在休眠(sleep(10)),等它醒來時,mutex 將會永遠被鎖住,這就是為什麼 thread1 陷入無休止的阻塞中。
6. 如何避免因此產生的死結
由於線程清理函數 pthread_cleanup_push 使用的策略是先進後出(FILO),那麼我們可以在 pthread_cond_wait 函數前先註冊一個線程處理函數:
[code:c]
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);
}[/code]
這樣,當線程被取消時,先執行 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://blog.solrex.cn/go/623339.html