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()函數。ide
(3)線程函數自己調用pthread_exit()。函數返回線程退出後傳出來的retval指針。函數
【說明】spa
1. pthread_exit()函數的參數retval是一個無類型指針,這與pthread_create函數中傳給啓動例程的單個參數相似。進程中的其餘線程能夠經過調用pthread_join函數來訪問到這個指針。線程
2. 調用pthread_join()函數將一直阻塞,直到指定的線程(參數thread)終止,終止方式是上面所描述的3種方式。指針
(1) 若是線程簡單地從啓動例程中返回,即執行return語句,pthread_join函數的參數value_ptr就包含返回碼。code
(2) 若是線程被其餘線程取消,pthread_join函數的參數value_ptr指向的內存單元就被設置爲PTHREAD_CANCELED。blog
(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 }
## 運行結果:
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; }
## 運行結果:
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 }
## 運行結果:
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 }
## 運行結果:
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是直接執行return語句終止線程的,即return ((void*)11); 沒有執行到pthread_cleanup_pop(0); 線程就終止了,並無執行在pthread_cleanup_push函數中註冊的清理函數cleanup,由於它不知足註冊的清理函數被調用的那3個條件中的任何一個,因此線程1的退出碼爲11,即thread 1 exit code: 11。
二、線程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是執行到pthread_exit((void*)32); 時線程終止,而且在線程函數體中是執行了兩個pthread_cleanup_pop(0); 語句的,因此pthread_cleanup_pop函數會刪除掉在前面的pthread_cleanup_push中註冊的清理函數cleanup,可是不會執行清理處理函數,線程3的退出碼爲:thread 3 exit code: 32。
綜上所述,能夠得出如下結論:
一、若是線程是經過從它的啓動例程中調用return語句而終止的話,它的清理處理程序就不會被調用。
二、清理處理程序是按照與它們註冊時相反的順序被調用的。
進程和線程原語的比較
在默認狀態下,線程的終止狀態會保存直到對該線程調用pthread_join。可是若是線程已經被分離,線程的底層存儲資源能夠在線程終止時當即被收回。在線程被分離後,咱們不能用pthread_join函數等待它的終止狀態,由於對分離線程調用pthread_join會產生未定義的行爲。分離線程能夠調用pthread_detach()函數。
//線程分離函數 int pthread_detach(pthread_t thread);【參數】thread:待分離的線程ID值。【返回值】成功,返回0;失敗,返回錯誤碼。