終止正在執行的子執行緒(一、幾種方式的介紹)
最近開發的東西有涉及到執行緒的建立和釋放,由於對這一塊不是很熟悉,查閱很多資料,現記錄如下:
如何正確的終止正在執行的子執行緒
最近開發一些東西,執行緒數非常之多,當用戶輸入Ctrl+C的情形下,預設的訊號處理會把程式退出,這時有可能會有很多執行緒的資源沒有得到很好的釋放,造成了記憶體洩露等等諸如此類的問題,所以,就如上述使用場景一般,如何從待外部安全的終止正在執行的執行緒,就是本文要討論的內容。
首先我們來看一下,讓當前正在執行的子執行緒停止的所有方法
1.任何一個執行緒呼叫exit
2.pthread_exit
3.pthread_kill
4.pthread_cancel
PS:下面的程式碼都會用到一個函式checkResults:
static void checkResults(char *string, int rc) { if (rc) { printf("Error on : %s, rc=%d", string, rc); exit(EXIT_FAILURE); } return; }
下面進行一一分析:
1)任何一個執行緒呼叫exit
任何一個執行緒只要呼叫了exit都會導致程序結束,各種子執行緒當然也能很好的結束了,可是這種退出會有一個資源釋放的問題.我們知道當一個程序終止時,核心對該程序所有尚未關閉的檔案描述符呼叫
close關閉,所以即使使用者程式不呼叫close,在終止時核心也會自動關閉它開啟的所有檔案。標準C++ IO流也會很好的在exit退出時得到flush並且釋放資源,這些東西並不會造成資源的浪費(系統呼叫main函式入口類似於exit(main(argc,argv))).實際上,無論程序怎樣結束,系統都將會釋放掉所有程式碼所申請的資源,無論是堆上的還是棧上的。這種結束所有執行緒(包括主執行緒)的方式實際上在很多時候是非常可取的,但是對於針對關閉時進行一些別的邏輯的處理(指非資源釋放邏輯)就不會很好,例如我想在程式被kill掉之前統計一下完成了多少的工作,這個統計類似於MapReduce,需要去每個執行緒獲取,並且最後歸併成一個統一的結果等等場景。2) pthread_exit
當前執行的執行緒執行到pthread_exit後直接退出執行緒。對於各個子執行緒能夠清楚地知道自己在什麼時候結束的情景下,這個函式非常好用。可是實際上很多時候一個執行緒不知道自己在什麼時候會結束,例如遭遇Ctrl+C時,kill程序時。當然如果排除所有的外界干擾的話,那就讓每個執行緒幹完自己的事情後,然後自覺地乖乖的呼叫pthread_exit就可以了。
這裡還有一種方法,既然子執行緒可以通過pthread_exit來正確退出,那麼我們可以在遭遇Ctrl+C時,kill程序時處理signal訊號,然後分別給在某一個執行緒可以訪問的公共區域存上一個flag變數,執行緒內部每執行一段時間(很短)來檢查一下flag,若發現需要終止自己時,自己呼叫pthread_exit,此法有一個弱點就是當子執行緒需要進行阻塞的操作時,可能無暇顧及檢查flag,例如socket阻塞操作。如果你的子執行緒的任務基本沒有非阻塞的函式,那麼這麼做也不失為一種很好的方案。
3) pthread_kill
不要被這個可怕的邪惡的名字所嚇倒,其實pthread_kill並不像他的名字那樣威力大,使用之後,你會感覺,他徒有虛名而已。
pthread_kill的職責其實只是向指定的執行緒傳送signal訊號而已,並沒有真正的kill掉一個執行緒,當然這裡需要說明一下,有些訊號的預設行為就是exit,那此時你使用pthread_kill傳送訊號給目標執行緒,目標執行緒會根據這個訊號的預設行為進行操作,有可能是exit。當然我們同時也可以更改獲取某個訊號的行為,以此來達到我們終止子執行緒的目的。
#define _MULTI_THREADED #include <pthread.h> #include <stdio.h> #include <signal.h> #include "check.h" /* struct sigaction { __sighandler_t sa_handler; unsigned long sa_flags; void (*sa_restorer)(void); sigset_t sa_mask; /* mask last for extensibility */ }; */ void sighand(int signo); void *threadfunc(void *parm) { pthread_t self = pthread_self(); pthread_id_np_t tid; int rc; pthread_getunique_np(&self, &tid); printf("Thread 0x%.8x %.8x entered\n", tid); errno = 0; rc = sleep(30); if (rc != 0 && errno == EINTR) { printf("Thread 0x%.8x %.8x got a signal delivered to it\n", tid); return NULL; } printf("Thread 0x%.8x %.8x did not get expected results! rc=%d, errno=%d\n", tid, rc, errno); return NULL; } int main(int argc, char **argv) { int rc; int i; struct sigaction actions; pthread_t threads; printf("Enter Testcase - %s\n", argv[0]); printf("Set up the alarm handler for the process\n"); memset(&actions, 0, sizeof(actions)); sigemptyset(&actions.sa_mask); actions.sa_flags = 0; actions.sa_handler = sighand; rc = sigaction(SIGALRM,&actions,NULL); checkResults("sigaction\n", rc); rc = pthread_create(&threads, NULL, threadfunc, NULL); checkResults("pthread_create()\n", rc); sleep(3); rc = pthread_kill(threads, SIGALRM); checkResults("pthread_kill()\n", rc); rc = pthread_join(threads, NULL); checkResults("pthread_join()\n", rc); printf("Main completed\n"); return 0; } void sighand(int signo) { pthread_t self = pthread_self(); pthread_id_np_t tid; pthread_getunique_np(&self, &tid); printf("Thread 0x%.8x %.8x in signal handler\n", tid); return; }
執行結果:
可見,我們可以通過截獲的signal訊號,來釋放掉執行緒申請的資源,可是遺憾的是我們不能在signal處理函式內呼叫pthread_exit來終結掉我們想要終結的執行緒,因為pthread_exit只能終結當前執行緒,而signal被呼叫的方式可以理解為核心的回撥,不是在同一個執行緒執行的,所以這裡只能做處理釋放資源的事情,而執行緒內部只能通過判斷有沒有被中斷(一般是EINTR)來決定是否要求自己結束,判定後可以呼叫pthread_exit退出。Output: Enter Testcase - QP0WTEST/TPKILL0 Set up the alarm handler for the process Thread 0x00000000 0000000c entered Thread 0x00000000 0000000c in signal handler Thread 0x00000000 0000000c got a signal delivered to it Main completed
此法對於一般的操作也是非常可行的,可是在有的情況下就不是一個比較好的方法了,比如我們有一些執行緒在處理網路IO事件,假設它是一種一個客戶端對應一個伺服器執行緒,阻塞從Socket中讀訊息的情況。我們一般在網路IO的庫裡面會加上對EINTR訊號的處理,例如recv時發現返回值小於0,檢查error後,會進行他對應的操作。有可能它會再recv一次,那就相當於我的執行緒根本就不會終止,因為網路IO的類有可能不知道在獲取EINTR時要終止執行緒。也就是說這不是一個特別好的可移植方案,如果你執行緒裡的操作使用了很多外來的不太熟悉的類,而且你並不知道它對EINTR的處理手段是什麼,這時你在使用這樣的方法來終止就有可能出問題了。而且如果你不是特別熟悉這方面的話你會很苦惱,“為什麼我的測試程式碼全是ok的,一加入你們部門開發的框架進來就不ok了,肯定是你們框架出問題了”。好了,為了不必要的麻煩,我最後沒有使用這個方案。
3)pthread_cancel
這個方案是我最終採用的方案,我認為是解決這個問題,相比較而言,通用的最好的解決方案。
pthread_cancel可以單獨使用,因為在很多系統函式裡面本身就有很多的斷點,當呼叫這些系統函式時就會命中其內部的斷點來結束執行緒。如下面的程式碼中,即便註釋掉我們自己設定的斷點pthread_testcancel()程式還是一樣的會被成功的cancel掉,因為printf函式內部有取消點(如果想了解更多的函式的取消點情況,可以閱讀《Unix高階環境程式設計》的執行緒部分)
#include <pthread.h> #include <stdio.h> #include<stdlib.h> #include <unistd.h> void *threadfunc(void *parm) { printf("Entered secondary thread\n"); while (1) { printf("Secondary thread is looping\n"); pthread_testcancel(); sleep(1); } return NULL; } int main(int argc, char **argv) { pthread_t thread; int rc=0; printf("Entering testcase\n"); /* Create a thread using default attributes */ printf("Create thread using the NULL attributes\n"); rc = pthread_create(&thread, NULL, threadfunc, NULL); checkResults("pthread_create(NULL)\n", rc); /* sleep() is not a very robust way to wait for the thread */ sleep(1); printf("Cancel the thread\n"); rc = pthread_cancel(thread); checkResults("pthread_cancel()\n", rc); /* sleep() is not a very robust way to wait for the thread */ sleep(10); printf("Main completed\n"); return 0; }
輸出:
POSIX保證了絕大部分的系統呼叫函式內部有取消點,我們看到很多在cancel呼叫的情景下,recv和send函式最後都會設定pthread_testcancel()取消點,其實這不是那麼有必要的。那麼究竟什麼時候該pthread_testcancel()出場呢?在《Unix高階環境程式設計》中有說,當遇到大量的基礎計算時(如科學計算),需要自己來設定取消點。Entering testcase Create thread using the NULL attributes Entered secondary thread Secondary thread is looping Cancel the thread Main completed
ok,得益於pthread_cancel,我們很輕鬆的把執行緒可以cancel掉,可是我們的資源呢?何時釋放...
下面來看兩個pthread函式
這兩個函式能夠保證在函式pthread_cleanup_push呼叫之後,函式pthread_cleanup_pop呼叫之前,任何形式的執行緒結束後都會呼叫之前向pthread_cleanup_push註冊的回撥函式void pthread_cleanup_push(void (*routine)(void *), void *arg); void pthread_cleanup_pop(int execute);
另外我們還可通過下面這個函式來設定一些狀態
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_DISABLE
PTHREAD_CANCEL_DEFERRED
asynchronous
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_ASYNCHRONOUS
當設定type為PTHREAD_CANCEL_ASYNCHRONOUS時,執行緒並不會等待命中取消點才結束,而是立馬結束。
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> int footprint=0; char *storage; void freerc(void *s) { free(s); puts("the free called"); } void *thread(void *arg) { int rc=0, oldState=0; rc = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); //close the cancel switch checkResults("pthread_setcancelstate()\n", rc); if ((storage = (char*) malloc(80)) == NULL) { perror("malloc() failed"); exit(6); } rc = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&oldState); //open the cancel switch checkResults("pthread_setcancelstate(2)\n", rc); /* Plan to release storage even if thread doesn't exit normally */ pthread_cleanup_push(freerc, storage); /*the freerc is method here, you can use your own method*/ puts("thread has obtained storage and is waiting to be cancelled"); footprint++; while (1) { pthread_testcancel(); //make a break point here //pthread_exit(NULL); //test exit to exam whether the freerc method called sleep(1); } pthread_cleanup_pop(1); } main() { pthread_t thid; void *status=NULL; if (pthread_create(&thid, NULL, thread, NULL) != 0) { perror("pthread_create() error"); exit(1); } while (footprint == 0) sleep(1); puts("IPT is cancelling thread"); if (pthread_cancel(thid) != 0) { perror("pthread_cancel() error"); sleep(2); exit(3); } if (pthread_join(thid, &status) != 0) { if(status != PTHREAD_CANCELED){ perror("pthread_join() error"); exit(4); } } if(status == PTHREAD_CANCELED) puts("PTHREAD_CANCELED"); puts("main exit"); }