POSIX線程終止相關函數 線程終止方式 單個線程可以通過3種方式退出,可以在不終止整個進程的情況下,停止線程的控制流。 (1)線程可以直接從啟動常式(也就是線程函數)中返回,即執行return語句,返回值是線程的退出碼。 (2)線程可以被同一進程中的其他線程取消。即其他線程調用pthread_ca ...
POSIX線程終止相關函數
//頭文件
#include <pthread.h>
//API函數
int pthread_join(pthread_t thread, void **value_ptr); void pthread_exit(void *retval); int pthread_cancel(pthread_t thread); void pthread_cleanup_push(void (*routine)(void*), void *arg); void pthread_cleanup_pop(int execute);
線程終止方式
單個線程可以通過3種方式退出,可以在不終止整個進程的情況下,停止線程的控制流。
(1)線程可以直接從啟動常式(也就是線程函數)中返回,即執行return語句,返回值是線程的退出碼。
(2)線程可以被同一進程中的其他線程取消。即其他線程調用pthread_cancel()函數。
(3)線程函數本身調用pthread_exit()。函數返回線程退出後傳出來的retval指針。
【說明】
1. pthread_exit()函數的參數retval是一個無類型指針,這與pthread_create函數中傳給啟動常式的單個參數類似。進程中的其他線程可以通過調用pthread_join函數來訪問到這個指針。
2. 調用pthread_join()函數將一直阻塞,直到指定的線程(參數thread)終止,終止方式是上面所描述的3種方式。
(1) 如果線程簡單地從啟動常式中返回,即執行return語句,pthread_join函數的參數value_ptr就包含返回碼。
(2) 如果線程被其他線程取消,pthread_join函數的參數value_ptr指向的記憶體單元就被設置為PTHREAD_CANCELED。
(3) 如果線程是調用pthread_exit()函數退出的,pthread_join函數的參數value_ptr將能獲取到pthread_exit()函數返回的retval指針。
3. 如果對線程的返回值不感興趣,可以將pthread_exit函數的retval置為NULL,這種情況下,pthread_join()函數仍可以等待指定的線程終止,但並不會獲取到線程的終止狀態。
實例1:獲取已終止的線程的退出碼。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 void* thr_fn1(void *arg) 6 { 7 printf("thread1 returning***\n"); 8 return (void*)1; 9 } 10 11 void* thr_fn2(void *arg) 12 { 13 printf("thread2 exiting###\n"); 14 pthread_exit((void*)2); 15 } 16 17 int main(int argc, char *argv[]) 18 { 19 int err; 20 pthread_t tid1, tid2; 21 void *tret; 22 23 if((err=pthread_create(&tid1, NULL, thr_fn1, NULL) != 0)){ 24 printf("Error: can`t create thread1!\n"); 25 } 26 if((err=pthread_create(&tid2, NULL, thr_fn2, NULL) != 0)){ 27 printf("Error: can`t create thread2!\n"); 28 } 29 30 if((err=pthread_join(tid1, &tret)) != 0){ 31 printf("Error: can`t join with thread1!\n"); 32 } 33 printf("thread1 exit code: %d\n", (int*)tret); 34 if((err=pthread_join(tid2, &tret)) != 0){ 35 printf("Error: can`t join with thread2!\n"); 36 } 37 printf("thread2 exit code: %d\n", (int*)tret); 38 39 return 0; 40 }View Code
## 運行結果:
thread1 returning***
thread2 exiting###
thread1 exit code: 1
thread2 exit code: 2
【分析】從運行結果可以看出,當一個線程通過調用pthread_exit退出或者簡單地從啟動常式中返回(return語句),進程中的其他線程可以通過調用pthread_join函數獲得該進程的退出狀態。
【說明】pthread_create和pthread_exit函數的無類型指針參數可以傳遞的值不止一個,這個指針可以是包含複雜信息的結構的地址,但是註意的是,這個結構指針指向的記憶體空間在調用者(線程函數)完成調用以後仍然是有效的。例如,在調用線程的棧區上分配了該結構,那麼其他線程在使用這個結構時記憶體內容可能已經改變了。又如,線程在自己的棧區上分配了一個結構,然後把指向這個結構的指針傳給pthread_exit,那麼調用pthread_join的線程試圖使用該結構時,這個棧區有可能已經被撤銷,這塊記憶體也已另作他用。
實例2:用局部(自動)變數(在棧區上分配的變數)作為pthread_exit的參數時出現的問題。
#include <stdio.h> #include <unistd.h> #include <pthread.h> typedef struct foo{ int a,b,c,d; }FOO; void printfoo(const char *s, const FOO *fp) { printf("%s", s); printf(" structure at %p\n", fp); printf(" foo.a = %d\n", fp->a); printf(" foo.b = %d\n", fp->b); printf(" foo.c = %d\n", fp->c); printf(" foo.d = %d\n", fp->d); } void* thr_fn1(void *arg) { FOO foo={1,2,3,4}; printf("thread 1: thread_id=%lu\n", pthread_self()); printfoo("thread 1:\n", &foo); printf("****** thread 1 exiting\n"); pthread_exit((void*)&foo); } void* thr_fn2(void *arg) { printf("thread 2: thread_id=%lu\n", pthread_self()); printf("###### thread 2 exiting\n"); pthread_exit((void*)2); } int main(int argc, char *argv[]) { int err; pthread_t tid1,tid2; FOO *fp; printf("Parent starting the first thread\n"); if((err=pthread_create(&tid1, NULL, thr_fn1, NULL) != 0)){ printf("Error: can`t create thread1!\n"); } if((err=pthread_join(tid1, (void*)&fp)) != 0){ printf("Error: can`t join with thread1!\n"); } sleep(1); printf("\nParent starting the second thread\n"); if((err=pthread_create(&tid2, NULL, thr_fn2, NULL) != 0)){ printf("Error: can`t create thread2!\n"); } sleep(1); printfoo("\nParent thread:\n", fp); return 0; }View Code
## 運行結果:
Parent starting the first thread
thread 1: thread_id=140128023041792
thread 1:
structure at 0x7f7219094f00
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
****** thread 1 exiting
Parent starting the second thread
thread 2: thread_id=140128023041792
###### thread 2 exiting
Parent thread:
structure at 0x7f7219094f00
foo.a = 420042496
foo.b = 32626
foo.c = 1
foo.d = 0
【分析】從運行結果可以看出,當主線程訪問局部結構時,結構的內容(線上程tid1的棧上分配的)已經發生改變了。即主線程試圖訪問已退出的tid1線程傳給它的結構時,由於該結構是線上程tid1的棧區上定義的,當線程退出時,棧區的記憶體空間也隨之釋放掉了,所以讀取到的內容是隨機值。為瞭解決這個問題,可以使用動態記憶體分配(malloc)或者使用全局結構。
線程取消機制
在預設情況下,pthread_cancel()函數會使得thread標識的線程的行為表現為如同調用了參數為PTHREAD_CANCELED的pthread_exit()函數,即pthread_exit(PTHREAD_CANCELED)。但是,線程可以選擇忽略取消或者控制如何被取消。【註意】pthread_cancel函數並不等待線程終止,它僅僅是提出請求。
實例3:線程取消的使用。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 int done = 0; 6 int cnt = 0; 7 8 void* thr_fn(void *arg) 9 { 10 //printf("new thread start\n"); //線程取消點 11 while(!done){ 12 cnt++; 13 if(cnt == 10) 14 pthread_testcancel(); //自己設置一個線程取消點 15 } 16 return ((void*)1); 17 } 18 19 int main(int argc, char *argv[]) 20 { 21 int err; 22 pthread_t tid; 23 void *tret; 24 25 if(0 != (err=pthread_create(&tid, NULL, thr_fn, NULL))){ 26 printf("Error: can`t create thread\n"); 27 return -1; 28 } 29 pthread_cancel(tid); 30 if(0 != (err=pthread_join(tid, &tret))){ 31 printf("Error: can`t join with thread\n"); 32 return -2; 33 } 34 printf("thread exit code: %d\n", (int*)tret); 35 printf("cnt = %d\n", cnt); 36 37 return 0; 38 }View Code
## 運行結果:
thread exit code: -1
cnt = 10
【分析】在主線程中調用了pthread_cancel(tid),線上程的啟動常式中,當cnt==10時,調用了pthread_testcancel()函數,這個函數是表示設置一個函數取消點。當線程運行到取消點的時候,線程就會終止。線程退出時的狀態碼為-1,說明瞭線程的退出是非正常退出的,而正常退出是的狀態碼應該是1。
【說明】線程在收到pthread_cancel的取消請求後,可能會忽略、立即取消線程或者運行至取消點再取消線程。系統預設情況下,收到取消請求後,線程會繼續運行,直到遇到下一個取消點處終止線程。
取消點:取消點是線程檢查它是否被取消的一個點,posix保證在一些函數中會自帶取消點,如sleep,accept,write,read,printf等,當執行上述函數時,自動觸發線程取消點,使線程終止。
【擴展】實際上,線程是否取消除了與取消點有關外,還和線程的取消狀態有關。取消狀態分為:PTHREAD_CANCEL_ENABLE(可取消狀態,這是系統預設的線程取消狀態);PTHREAD_CANCEL_DISABLE(不可取消狀態)。當線程的取消狀態是PTHREAD_CANCEL_DISABLE時,即使線程收到取消請求在取消點也不會取消線程,直到可取消狀態變更為PTHREAD_CANCEL_ENABLE時,線程才會在下一個取消點取消線程。
//設置線程取消點函數
void pthread_testcancel(void);
//修改線程的取消狀態函數 int pthread_setcancelstate(int state, int *oldstate);
【參數說明】
state:設置新狀態值。
oldstate:存放原先的取消狀態。
【函數說明】該函數會在函數內部設置一個取消點,調用該函數時,如果收到一個取消請求,且取消狀態是可取消的,就會立即將線程取消。如果取消狀態為不可取消,且沒有取消請求,就不會取消,直到兩者條件都滿足時才會取消函數。
線上程的屬性中還有一個屬性與線程的取消有關,即它的取消類型,之前我們所說的取消屬於推遲取消,即在調用pthread_cancel函數後,需要等到線程運行至一個取消點時,線程才會被取消而終止線程。
但是,還有一種取類型為非同步取消,即當調用pthread_cancel後,線程就會被立即取消,而不用等到線程運行至取消點時再取消線程,取消類型同取消狀態一樣可以修改。
//修改線程的取消類型函數 int pthread_setcanceltype(int type, int *oldtype);
【參數說明】
type:設置新的取消類型。
oldtype:存放原先的取消類型。
【函數說明】取消類型有:PTHREAD_CANCEL_DEFERRED、PTHREAD_CANCEL_ASYNCHRONOUS。
PTHREAD_CANCEL_DEFERRED:線程接收到取消請求後,直到運行至"取消點"後才取消線程。
PTHREAD_CANCEL_ASYNCHRONOUS:線程接收到取消請求後,立即取消線程。
<說明>線程的“取消狀態”和“取消類型”存在於任意一個新建線程中,包括主線程,預設設置是PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED。
線程清理處理程式
線程可以安排它退出時需要調用的函數,這與進程在退出時可以用atexit函數安排退出是類似的。這樣的函數被稱為線程處理清理程式(thread cleanup handler)。一個線程可以建立多個清理處理程式。處理程式記錄在棧中,也就是說它們的執行順序與它們註冊時相反。
//註冊線程清理處理程式 void pthread_cleanup_push(void (*rtn)(void*), void *arg);
【參數】
rtn:線程退出時被調用的清理函數。
arg:傳入給rtn的參數。 //解除線程清理處理程式 void pthread_cleanup_pop(int execute);
【說明】當線程執行以下動作時,清理函數rtn是由phtread_cleanup_push函數調度的,調用時只傳入一個參數arg。
- 線程函數調用pthread_exit時;
- 響應取消線程請求時;
- 用非零execute參數調用pthread_cleanup_pop時。
<註意> 如果pthread_cleanup_pop的execute參數如果設置為0,清理函數rtn將不被調用,也就是說,線程函數執行pthread_cleanup_pop(0)時,在phtread_cleanup_push中註冊的清理函數rtn將不被執行,但是
pthread_cleanup_pop函數仍將刪除上次在phtread_cleanup_push函數中註冊的清理處理程式(或函數)。
【擴展】這兩個函數有一個限制,因為它們可以實現為巨集,pthread_cleanup_push()與pthread_cleanup_pop()必須成對的出現線上程函數相同的作用域中。
pthread_cleanup_push的巨集定義可以包含字元 { ,這種情況下,在與pthread_cleanup_pop的巨集定義中要有對應的匹配字元 } 。示例如下:
#define pthread_cleanup_push(rtn,arg) { \ struct _pthread_handler_rec __cleanup_handler, **__head; \ __cleanup_handler.rtn = rtn; \ __cleanup_handler.arg = arg; \ (void) pthread_getspecific(_pthread_handler_key, &__head); \ __cleanup_handler.next = *__head; \ *__head = &__cleanup_handler; #define pthread_cleanup_pop(ex) \ *__head = __cleanup_handler.next; \ if (ex) (*__cleanup_handler.rtn)(__cleanup_handler.arg); \ }
如果pthread_cleanup_pop函數的參數execute設置為0,清理將不被調用。但不管發生上述哪種情況,pthread_cleanup_pop都將刪除上次pthread_cleanup_push調用建立的清理處理程式。示例如下:
pthread_cleanup_push(routine, &arg); ...... pthread_cleanup_pop(0); pthread_exit((void*)1);
<說明>當線程函數執行到pthread_exit函數時,pthread_cleanup_pop函數將解除pthread_cleanup_push函數註冊的清理處理函數routine,但是不會執行routine中的函數體代碼。
實例4:使用線程清理處理程式。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 void cleanup(void *arg) 6 { 7 printf("cleanup: %s\n", (char*)arg); 8 } 9 10 void* thr_fn1(void *arg) 11 { 12 printf("thread 1 start\n"); 13 pthread_cleanup_push(cleanup, "thread 1 first handler"); 14 pthread_cleanup_push(cleanup, "thread 1 secend handler"); 15 printf("thread 1 push complete\n"); 16 if(arg) 17 return ((void*)11); 18 pthread_cleanup_pop(0); 19 pthread_cleanup_pop(0); 20 return ((void*)12); 21 } 22 23 void* thr_fn2(void *arg) 24 { 25 printf("thread 2 start\n"); 26 pthread_cleanup_push(cleanup, "thread 2 first handler"); 27 pthread_cleanup_push(cleanup, "thread 2 secend handler"); 28 printf("thread 2 push complete\n"); 29 if(arg) 30 pthread_exit((void*)21); 31 pthread_cleanup_pop(0); 32 pthread_cleanup_pop(0); 33 pthread_exit((void*)22); 34 } 35 36 void* thr_fn3(void *arg) 37 { 38 printf("thread 3 start\n"); 39 pthread_cleanup_push(cleanup, "thread 3 first handler"); 40 pthread_cleanup_push(cleanup, "thread 3 secend handler"); 41 printf("thread 3 push complete\n"); 42 if(arg) 43 pthread_exit((void*)31); 44 pthread_cleanup_pop(0); 45 pthread_cleanup_pop(0); 46 pthread_exit((void*)32); 47 } 48 49 int main(int argc, char *argv[]) 50 { 51 int err; 52 pthread_t tid1, tid2, tid3; 53 void *tret; 54 55 if(0 != (err = pthread_create(&tid1, NULL, thr_fn1, (void*)1) )){ 56 printf("Error: can`t create thread 1\n"); 57 } 58 if(0 != (err = pthread_create(&tid2, NULL, thr_fn2, (void*)1) )){ 59 printf("Error: can`t create thread 2\n"); 60 } 61 if(0 != (err = pthread_create(&tid3, NULL, thr_fn3, NULL) )){ 62 printf("Error: can`t create thread 3\n"); 63 } 64 65 if(0 != (err = pthread_join(tid1, &tret))){ 66 printf("Error: can`t join with thread 1\n"); 67 } 68 printf("thread 1 exit code: %d\n", (int*)tret); 69 if(0 != (err = pthread_join(tid2, &tret))){ 70 printf("Error: can`t join with thread 2\n"); 71 } 72 printf("thread 2 exit code: %d\n", (int*)tret); 73 if(0 != (err = pthread_join(tid3, &tret))){ 74 printf("Error: can`t join with thread 3\n"); 75 } 76 printf("thread 3 exit code: %d\n", (int*)tret); 77 78 return 0; 79 }View Code
## 運行結果:
thread 1 start
thread 1 push complete
thread 1 exit code: 11
thread 2 start
thread 2 push complete
cleanup: thread 2 secend handler
cleanup: thread 2 first handler
thread 3 start
thread 3 push complete
thread 2 exit code: 21
thread 3 exit code: 32
## 分析:
1、線程1是直接執行return語句終止線程的,即return ((void*)11); 沒有執行到pthread_cleanup_pop(0); 線程就終止了,並沒有執行在pthread_cleanup_push函數中註冊的清理函數cleanup,因為它不滿足註冊的清理函數被調用的那3個條件中的任何一個,所以線程1的退出碼為11,即thread 1 exit code: 11。
2、線程2是執行到pthread_exit((void*)21); 時線程就終止了,滿足已註冊的清理函數被調用的條件。這時將調用在pthread_cleanup_push中註冊的清理函數cleanup。從運行結果中可以看到,調用順序和註冊順序是相反的,這是因為清理函數是記錄在棧中的,而棧是一種先進後出的數據結構。特別值得註意的是,
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void*)22);
線上程2的啟動常式函數體中,上面的3條語句是沒有執行到的,從線程2的退出碼結果為:thread 2 exit code: 21 可以證明這一點。
3、線程3是執行到pthread_exit((void*)32); 時線程終止,並且線上程函數體中是執行了兩個pthread_cleanup_pop(0); 語句的,所以pthread_cleanup_pop函數會刪除掉在前面的pthread_cleanup_push中註冊的清理函數cleanup,但是不會執行清理處理函數,線程3的退出碼為:thread 3 exit code: 32。
綜上所述,可以得出以下結論:
1、如果線程是通過從它的啟動常式中調用return語句而終止的話,它的清理處理程式就不會被調用。
2、清理處理程式是按照與它們註冊時相反的順序被調用的。
進程和線程原語的比較
在預設狀態下,線程的終止狀態會保存直到對該線程調用pthread_join。但是如果線程已經被分離,線程的底層存儲資源可以線上程終止時立即被收回。線上程被分離後,我們不能用pthread_join函數等待它的終止狀態,因為對分離線程調用pthread_join會產生未定義的行為。分離線程可以調用pthread_detach()函數。
//線程分離函數 int pthread_detach(pthread_t thread);
【參數】thread:待分離的線程ID值。
【返回值】成功,返回0;失敗,返回錯誤碼。