最近開發一些東西,線程數非常之多,當用戶輸入Ctrl+C的情形下,默認的信號處理會把程序退出,這時有可能會有很多線程的資源沒有得到很好的釋放,造成了內存泄露等等諸如此類的問題,本文就是圍繞着這么一個使用場景討論如何正確的終止正在運行的子線程。其實本文更確切的說是解決如何從待終止線程外部安全的終止正在運行的線程
首先我們來看一下,讓當前正在運行的子線程停止的所有方法
1.任何一個線程調用exit
2.pthread_exit
3.pthread_kill
4.pthread_cancel
下面我們一一分析各種終止正在運行的程序的方法
任何一個線程調用exit
任何一個線程只要調用了exit都會導致進程結束,各種子線程當然也能很好的結束了,可是這種退出會有一個資源釋放的問題.我們知道當一個進程終止時,內核對該進程所有尚未關閉的文件描述符調用close關閉,所以即使用戶程序不調用close,在終止時內核也會自動關閉它打開的所有文件。沒錯,標准C++ IO流也會很好的在exit退出時得到flush並且釋放資源,這些東西並不會造成資源的浪費(系統調用main函數入口類似於exit(main(argc,argv))).表面上似乎所有的問題都能隨着進程的結束來得到很好的處理,其實並不然,我們程序從堆上分配的內存就不能得到很好的釋放,如new ,delete后的存儲空間,這些空間進程結束並不會幫你把這部分內存歸還給內存.(本文初稿時,因基礎不牢固,此處寫錯,事實上無論進程這樣結束,系統都將會釋放掉所有代碼所申請的資源,無論是堆上的還是棧上的。(感謝ZKey的指導)。這種結束所有線程(包括主線程)的方式實際上在很多時候是非常可取的,但是對於針對關閉時進行一些別的邏輯的處理(指非資源釋放邏輯)就不會很好,例如我想在程序被kill掉之前統計一下完成了多少的工作,這個統計類似於MapReduce,需要去每個線程獲取,並且最后歸並程一個統一的結果等等場景)
pthread_exit
此函數的使用場景是當前運行的線程運行pthread_exit得到退出,對於各個子線程能夠清楚地知道自己在什么時候結束的情景下,非常好用,可是實際上往往很多時候一個線程不能知道知道在什么時候該結束,例如遭遇Ctrl+C時,kill進程時,當然如果排除所有的外界干擾的話,那就讓每個線程干完自己的事情后,然后自覺地乖乖的調用pthread_exit就可以了,這並不是本文需要討論的內容,本文的情景就是討論如何處理特殊情況。
這里還有一種方法,既然子線程可以通過pthread_exit來正確退出,那么我們可以在遭遇Ctrl+C時,kill進程時處理signal信號,然后分別給在某一個線程可以訪問的公共區域存上一個flag變量,線程內部每運行一段時間(很短)來檢查一下flag,若發現需要終止自己時,自己調用pthread_exit,此法有一個弱點就是當子線程需要進行阻塞的操作時,可能無暇顧及檢查flag,例如socket阻塞操作。如果你的子線程的任務基本沒有非阻塞的函數,那么這么干也不失為一種很好的方案。
pthread_kill
不要被這個可怕的邪惡的名字所嚇倒,其實pthread_kill並不像他的名字那樣威力大,使用之后,你會感覺,他徒有虛名而已
pthread_kill的職責其實只是向指定的線程發送signal信號而已,並沒有真正的kill掉一個線程,當然這里需要說明一下,有些信號的默認行為就是exit,那此時你使用pthread_kill發送信號給目標線程,目標線程會根據這個信號的默認行為進行操作,有可能是exit。當然我們同時也可以更改獲取某個信號的行為,以此來達到我們終止子線程的目的。
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <signal.h>
5 #include " check.h "
6
7 #define NUMTHREADS 3
8 void sighand( int signo);
9
10 void *threadfunc( void *parm)
11 {
12 pthread_t self = pthread_self();
13 pthread_id_np_t tid;
14 int rc;
15
16 pthread_getunique_np(&self, &tid);
17 printf( " Thread 0x%.8x %.8x entered\n " , tid);
18 errno = 0 ;
19 rc = sleep( 30 );
20 if (rc != 0 && errno == EINTR) {
21 printf( " Thread 0x%.8x %.8x got a signal delivered to it\n " ,
22 tid);
23 return NULL;
24 }
25 printf( " Thread 0x%.8x %.8x did not get expected results! rc=%d, errno=%d\n " ,
26 tid, rc, errno);
27 return NULL;
28 }
29
30 int main( int argc, char **argv)
31 {
32 int rc;
33 int i;
34 struct sigaction actions;
35 pthread_t threads[NUMTHREADS];
36
37 printf( " Enter Testcase - %s\n " , argv[ 0 ]);
38
39 printf( " Set up the alarm handler for the process\n " );
40 memset(&actions, 0 , sizeof (actions));
41 sigemptyset(&actions.sa_mask);
42 actions.sa_flags = 0 ;
43 actions.sa_handler = sighand;
44
45 rc = sigaction(SIGALRM,&actions,NULL);
46 checkResults( " sigaction\n " , rc);
47
48 for (i= 0 ; i<NUMTHREADS; ++i) {
49 rc = pthread_create(&threads[i], NULL, threadfunc, NULL);
50 checkResults( " pthread_create()\n " , rc);
51 }
52
53 sleep( 3 );
54 for (i= 0 ; i<NUMTHREADS; ++i) {
55 rc = pthread_kill(threads[i], SIGALRM);
56 checkResults( " pthread_kill()\n " , rc);
57 }
58
59 for (i= 0 ; i<NUMTHREADS; ++i) {
60 rc = pthread_join(threads[i], NULL);
61 checkResults( " pthread_join()\n " , rc);
62 }
63 printf( " Main completed\n " );
64 return 0 ;
65 }
66
67 void sighand( int signo)
68 {
69 pthread_t self = pthread_self();
70 pthread_id_np_t tid;
71
72 pthread_getunique_np(&self, &tid);
73 printf( " Thread 0x%.8x %.8x in signal handler\n " ,
74 tid);
75 return ;
76 }
運行輸出為:
2
3 Enter Testcase - QP0WTEST/TPKILL0
4 Set up the alarm handler for the process
5 Thread 0x00000000 0000000c entered
6 Thread 0x00000000 0000000d entered
7 Thread 0x00000000 0000000e entered
8 Thread 0x00000000 0000000c in signal handler
9 Thread 0x00000000 0000000c got a signal delivered to it
10 Thread 0x00000000 0000000d in signal handler
11 Thread 0x00000000 0000000d got a signal delivered to it
12 Thread 0x00000000 0000000e in signal handler
13 Thread 0x00000000 0000000e got a signal delivered to it
14 Main completed
我們可以通過截獲的signal信號,來釋放掉線程申請的資源,可是遺憾的是我們不能再signal處理里調用pthread_exit來終結掉線程,因為pthread_exit是中介當前線程,而signal被調用的方式可以理解為內核的回調,不是在同一個線程運行的,所以這里只能做處理釋放資源的事情,線程內部只有判斷有沒有被中斷(一般是EINTR)來斷定是否要求自己結束,判定后可以調用pthread_exit退出。
此法對於一般的操作也是非常可行的,可是在有的情況下就不是一個比較好的方法了,比如我們有一些線程在處理網絡IO事件,假設它是一種一個客戶端對應一個服務器線程,阻塞從Socket中讀消息的情況。我們一般在網絡IO的庫里面回家上對EINTR信號的處理,例如recv時發現返回值小於0,檢查error后,會進行他對應的操作。有可能他會再recv一次,那就相當於我的線程根本就不回終止,因為網絡IO的類有可能不知道在獲取EINTR時要終止線程。也就是說這不是一個特別好的可移植方案,如果你線程里的操作使用了很多外來的不太熟悉的類,而且你並不是他對EINTR的處理手段是什么,這是你在使用這樣的方法來終止就有可能出問題了。而且如果你不是特別熟悉這方面的話你會很苦惱,“為什么我的測試代碼全是ok的,一加入你們部門開發的框架進來就不ok了,肯定是你們框架出問題了”。好了,為了不必要的麻煩,我最后沒有使用這個方案。
pthread_cancel
這個方案是我最終采用的方案,我認為是解決這個問題,通用的最好的解決方案,雖然前面其他方案的有些問題他可能也不好解決,但是相比較而言,還是相當不錯的
pthread_cancel可以單獨使用,因為在很多系統函數里面本身就有很多的斷點,當調用這些系統函數時就會命中其內部的斷點來結束線程,如下面的代碼中,即便注釋掉我們自己設置的斷點pthread_testcancel()程序還是一樣的會被成功的cancel掉,因為printf函數內部有取消點(如果大家想了解更多的函數的取消點情況,可以閱讀《Unix高級環境編程》的線程部分)
2 #include <stdio.h>
3 #include<stdlib.h>
4 #include <unistd.h>
5 void *threadfunc( void *parm)
6 {
7 printf( " Entered secondary thread\n " );
8 while ( 1 ) {
9 printf( " Secondary thread is looping\n " );
10 pthread_testcancel();
11 sleep( 1 );
12 }
13 return NULL;
14 }
15
16 int main( int argc, char **argv)
17 {
18 pthread_t thread;
19 int rc= 0 ;
20
21 printf( " Entering testcase\n " );
22
23 /* Create a thread using default attributes */
24 printf( " Create thread using the NULL attributes\n " );
25 rc = pthread_create(&thread, NULL, threadfunc, NULL);
26 checkResults( " pthread_create(NULL)\n " , rc);
27
28 /* sleep() is not a very robust way to wait for the thread */
29 sleep( 1 );
30
31 printf( " Cancel the thread\n " );
32 rc = pthread_cancel(thread);
33 checkResults( " pthread_cancel()\n " , rc);
34
35 /* sleep() is not a very robust way to wait for the thread */
36 sleep( 10 );
37 printf( " Main completed\n " );
38 return 0 ;
39 }
輸出:
Create thread using the NULL attributes
Entered secondary thread
Secondary thread is looping
Cancel the thread
Main completed
POSIX保證了絕大部分的系統調用函數內部有取消點,我們看到很多在cancel調用的情景下,recv和send函數最后都會設置pthread_testcancel()取消點,其實這不是那么有必要的,那么究竟什么時候該pthread_testcancel()出場呢?《Unix高級環境編程》也說了,當遇到大量的基礎計算時(如科學計算),需要自己來設置取消點。
ok,得益於pthread_cancel,我們很輕松的把線程可以cancel掉,可是我們的資源呢?何時釋放...
下面來看兩個pthread函數
1.void pthread_cleanup_push(void (*routine)(void *), void *arg);
2.void pthread_cleanup_pop(int execute);
這兩個函數能夠保證在 1函數調用之后,2函數調用之前的任何形式的線程結束調用向pthread_cleanup_push注冊的回調函數
另外我們還可通過下面這個函數來設置一些狀態
int pthread_setcanceltype(int type, int *oldtype);
| Cancelability | Cancelability State | Cancelability Type |
|---|---|---|
| disabled | PTHREAD_CANCEL_DISABLE | PTHREAD_CANCEL_DEFERRED |
| disabled | PTHREAD_CANCEL_DISABLE | PTHREAD_CANCEL_ASYNCHRONOUS |
| deferred | PTHREAD_CANCEL_ENABLE | PTHREAD_CANCEL_DEFERRED |
| asynchronous | PTHREAD_CANCEL_ENABLE | PTHREAD_CANCEL_ASYNCHRONOUS |
當我們設置type為PTHREAD_CANCEL_ASYNCHRONOUS時,線程並不會等待命中取消點才結束,而是立馬結束
好了,下面貼代碼:
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <errno.h>
6 int footprint= 0 ;
7 char *storage;
8 void freerc( void *s)
9 {
10 free(s);
11 puts( " the free called " );
12 }
13
14 static void checkResults( char * string , int rc) {
15 if (rc) {
16 printf( " Error on : %s, rc=%d " ,
17 string , rc);
18 exit(EXIT_FAILURE);
19 }
20 return ;
21 }
22
23 void *thread( void *arg) {
24 int rc= 0 , oldState= 0 ;
25 rc = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // close the cancel switch
26 checkResults( " pthread_setcancelstate()\n " , rc);
27 if ((storage = ( char *) malloc( 80 )) == NULL) {
28 perror( " malloc() failed " );
29 exit( 6 );
30 }
31 rc = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&oldState); // open the cancel switch
32 checkResults( " pthread_setcancelstate(2)\n " , rc);
33 /* Plan to release storage even if thread doesn't exit normally */
34
35 pthread_cleanup_push(freerc, storage); /* the free is method here you can use your own method here */
36
37 puts( " thread has obtained storage and is waiting to be cancelled " );
38 footprint++;
39 while ( 1 )
40 {
41 pthread_testcancel(); // make a break point here
42 // pthread_exit(NULL); // test exit to exam whether the freerc method called
43 sleep( 1 );
44 }
45
46 pthread_cleanup_pop( 1 );
47 }
48
49 main() {
50 pthread_t thid;
51 void *status=NULL;
52
53 if (pthread_create(&thid, NULL, thread, NULL) != 0 ) {
54 perror( " pthread_create() error " );
55 exit( 1 );
56 }
57
58 while (footprint == 0 )
59 sleep( 1 );
60
61 puts( " IPT is cancelling thread " );
62
63 if (pthread_cancel(thid) != 0 ) {
64 perror( " pthread_cancel() error " );
65 sleep( 2 );
66 exit( 3 );
67 }
68
69 if (pthread_join(thid, &status) != 0 ) {
70 if (status != PTHREAD_CANCELED){
71 perror( " pthread_join() error " );
72 exit( 4 );
73 }
74 }
75 if (status == PTHREAD_CANCELED)
76 puts( " PTHREAD_CANCELED " );
77
78 puts( " main exit " );
79 }
