C語(yǔ)言如何正確的終止正在運(yùn)行的子線程
最近開(kāi)發(fā)一些東西,線程數(shù)非常之多,當(dāng)用戶輸入Ctrl+C的情形下,默認(rèn)的信號(hào)處理會(huì)把程序退出,這時(shí)有可能會(huì)有很多線程的資源沒(méi)有得到很好的釋放,造成了內(nèi)存泄露等等諸如此類的問(wèn)題,本文就是圍繞著這么一個(gè)使用場(chǎng)景討論如何正確的終止正在運(yùn)行的子線程。其實(shí)本文更確切的說(shuō)是解決如何從待終止線程外部安全的終止正在運(yùn)行的線程
首先我們來(lái)看一下,讓當(dāng)前正在運(yùn)行的子線程停止的所有方法
1.任何一個(gè)線程調(diào)用exit
2.pthread_exit
3.pthread_kill
4.pthread_cancel
下面我們一一分析各種終止正在運(yùn)行的程序的方法
任何一個(gè)線程調(diào)用exit
任何一個(gè)線程只要調(diào)用了exit都會(huì)導(dǎo)致進(jìn)程結(jié)束,各種子線程當(dāng)然也能很好的結(jié)束了,可是這種退出會(huì)有一個(gè)資源釋放的問(wèn)題.我們知道當(dāng)一個(gè)進(jìn)程終止時(shí),內(nèi)核對(duì)該進(jìn)程所有尚未關(guān)閉的文件描述符調(diào)用close關(guān)閉,所以即使用戶程序不調(diào)用close,在終止時(shí)內(nèi)核也會(huì)自動(dòng)關(guān)閉它打開(kāi)的所有文件。沒(méi)錯(cuò),標(biāo)準(zhǔn)C++ IO流也會(huì)很好的在exit退出時(shí)得到flush并且釋放資源,這些東西并不會(huì)造成資源的浪費(fèi)(系統(tǒng)調(diào)用main函數(shù)入口類似于exit(main(argc,argv))).表面上似乎所有的問(wèn)題都能隨著進(jìn)程的結(jié)束來(lái)得到很好的處理,其實(shí)并不然,我們程序從堆上分配的內(nèi)存就不能得到很好的釋放,如new ,delete后的存儲(chǔ)空間,這些空間進(jìn)程結(jié)束并不會(huì)幫你把這部分內(nèi)存歸還給內(nèi)存.(本文初稿時(shí),因基礎(chǔ)不牢固,此處寫錯(cuò),事實(shí)上無(wú)論進(jìn)程這樣結(jié)束,系統(tǒng)都將會(huì)釋放掉所有代碼所申請(qǐng)的資源,無(wú)論是堆上的還是棧上的。(感謝ZKey的指導(dǎo))。這種結(jié)束所有線程(包括主線程)的方式實(shí)際上在很多時(shí)候是非常可取的,但是對(duì)于針對(duì)關(guān)閉時(shí)進(jìn)行一些別的邏輯的處理(指非資源釋放邏輯)就不會(huì)很好,例如我想在程序被kill掉之前統(tǒng)計(jì)一下完成了多少的工作,這個(gè)統(tǒng)計(jì)類似于MapReduce,需要去每個(gè)線程獲取,并且最后歸并程一個(gè)統(tǒng)一的結(jié)果等等場(chǎng)景)
pthread_exit
此函數(shù)的使用場(chǎng)景是當(dāng)前運(yùn)行的線程運(yùn)行pthread_exit得到退出,對(duì)于各個(gè)子線程能夠清楚地知道自己在什么時(shí)候結(jié)束的情景下,非常好用,可是實(shí)際上往往很多時(shí)候一個(gè)線程不能知道知道在什么時(shí)候該結(jié)束,例如遭遇Ctrl+C時(shí),kill進(jìn)程時(shí),當(dāng)然如果排除所有的外界干擾的話,那就讓每個(gè)線程干完自己的事情后,然后自覺(jué)地乖乖的調(diào)用pthread_exit就可以了,這并不是本文需要討論的內(nèi)容,本文的情景就是討論如何處理特殊情況。
這里還有一種方法,既然子線程可以通過(guò)pthread_exit來(lái)正確退出,那么我們可以在遭遇Ctrl+C時(shí),kill進(jìn)程時(shí)處理signal信號(hào),然后分別給在某一個(gè)線程可以訪問(wèn)的公共區(qū)域存上一個(gè)flag變量,線程內(nèi)部每運(yùn)行一段時(shí)間(很短)來(lái)檢查一下flag,若發(fā)現(xiàn)需要終止自己時(shí),自己調(diào)用pthread_exit,此法有一個(gè)弱點(diǎn)就是當(dāng)子線程需要進(jìn)行阻塞的操作時(shí),可能無(wú)暇顧及檢查flag,例如socket阻塞操作。如果你的子線程的任務(wù)基本沒(méi)有非阻塞的函數(shù),那么這么干也不失為一種很好的方案。
pthread_kill
不要被這個(gè)可怕的邪惡的名字所嚇倒,其實(shí)pthread_kill并不像他的名字那樣威力大,使用之后,你會(huì)感覺(jué),他徒有虛名而已
pthread_kill的職責(zé)其實(shí)只是向指定的線程發(fā)送signal信號(hào)而已,并沒(méi)有真正的kill掉一個(gè)線程,當(dāng)然這里需要說(shuō)明一下,有些信號(hào)的默認(rèn)行為就是exit,那此時(shí)你使用pthread_kill發(fā)送信號(hào)給目標(biāo)線程,目標(biāo)線程會(huì)根據(jù)這個(gè)信號(hào)的默認(rèn)行為進(jìn)行操作,有可能是exit。當(dāng)然我們同時(shí)也可以更改獲取某個(gè)信號(hào)的行為,以此來(lái)達(dá)到我們終止子線程的目的。
#define _MULTI_THREADED #include <pthread.h> #include <stdio.h> #include <signal.h> #include "check.h" #define NUMTHREADS 3 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[NUMTHREADS]; 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); for(i=0; i<NUMTHREADS; ++i) { rc = pthread_create(&threads[i], NULL, threadfunc, NULL); checkResults("pthread_create()\n", rc); } sleep(3); for(i=0; i<NUMTHREADS; ++i) { rc = pthread_kill(threads[i], SIGALRM); checkResults("pthread_kill()\n", rc); } for(i=0; i<NUMTHREADS; ++i) { rc = pthread_join(threads[i], 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; }
運(yùn)行輸出為:
Output:
Enter Testcase - QP0WTEST/TPKILL0
Set up the alarm handler for the process
Thread 0x00000000 0000000c entered
Thread 0x00000000 0000000d entered
Thread 0x00000000 0000000e entered
Thread 0x00000000 0000000c in signal handler
Thread 0x00000000 0000000c got a signal delivered to it
Thread 0x00000000 0000000d in signal handler
Thread 0x00000000 0000000d got a signal delivered to it
Thread 0x00000000 0000000e in signal handler
Thread 0x00000000 0000000e got a signal delivered to it
Main completed
我們可以通過(guò)截獲的signal信號(hào),來(lái)釋放掉線程申請(qǐng)的資源,可是遺憾的是我們不能再signal處理里調(diào)用pthread_exit來(lái)終結(jié)掉線程,因?yàn)閜thread_exit是中介當(dāng)前線程,而signal被調(diào)用的方式可以理解為內(nèi)核的回調(diào),不是在同一個(gè)線程運(yùn)行的,所以這里只能做處理釋放資源的事情,線程內(nèi)部只有判斷有沒(méi)有被中斷(一般是EINTR)來(lái)斷定是否要求自己結(jié)束,判定后可以調(diào)用pthread_exit退出。
此法對(duì)于一般的操作也是非??尚械?,可是在有的情況下就不是一個(gè)比較好的方法了,比如我們有一些線程在處理網(wǎng)絡(luò)IO事件,假設(shè)它是一種一個(gè)客戶端對(duì)應(yīng)一個(gè)服務(wù)器線程,阻塞從Socket中讀消息的情況。我們一般在網(wǎng)絡(luò)IO的庫(kù)里面回家上對(duì)EINTR信號(hào)的處理,例如recv時(shí)發(fā)現(xiàn)返回值小于0,檢查error后,會(huì)進(jìn)行他對(duì)應(yīng)的操作。有可能他會(huì)再recv一次,那就相當(dāng)于我的線程根本就不回終止,因?yàn)榫W(wǎng)絡(luò)IO的類有可能不知道在獲取EINTR時(shí)要終止線程。也就是說(shuō)這不是一個(gè)特別好的可移植方案,如果你線程里的操作使用了很多外來(lái)的不太熟悉的類,而且你并不是他對(duì)EINTR的處理手段是什么,這是你在使用這樣的方法來(lái)終止就有可能出問(wèn)題了。而且如果你不是特別熟悉這方面的話你會(huì)很苦惱,“為什么我的測(cè)試代碼全是ok的,一加入你們部門開(kāi)發(fā)的框架進(jìn)來(lái)就不ok了,肯定是你們框架出問(wèn)題了”。好了,為了不必要的麻煩,我最后沒(méi)有使用這個(gè)方案。
pthread_cancel
這個(gè)方案是我最終采用的方案,我認(rèn)為是解決這個(gè)問(wèn)題,通用的最好的解決方案,雖然前面其他方案的有些問(wèn)題他可能也不好解決,但是相比較而言,還是相當(dāng)不錯(cuò)的
pthread_cancel可以單獨(dú)使用,因?yàn)樵诤芏嘞到y(tǒng)函數(shù)里面本身就有很多的斷點(diǎn),當(dāng)調(diào)用這些系統(tǒng)函數(shù)時(shí)就會(huì)命中其內(nèi)部的斷點(diǎn)來(lái)結(jié)束線程,如下面的代碼中,即便注釋掉我們自己設(shè)置的斷點(diǎn)pthread_testcancel()程序還是一樣的會(huì)被成功的cancel掉,因?yàn)閜rintf函數(shù)內(nèi)部有取消點(diǎn)(如果大家想了解更多的函數(shù)的取消點(diǎn)情況,可以閱讀《Unix高級(jí)環(huán)境編程》的線程部分)
#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; }
輸出:
Entering testcase
Create thread using the NULL attributes
Entered secondary thread
Secondary thread is looping
Cancel the thread
Main completed
POSIX保證了絕大部分的系統(tǒng)調(diào)用函數(shù)內(nèi)部有取消點(diǎn),我們看到很多在cancel調(diào)用的情景下,recv和send函數(shù)最后都會(huì)設(shè)置pthread_testcancel()取消點(diǎn),其實(shí)這不是那么有必要的,那么究竟什么時(shí)候該pthread_testcancel()出場(chǎng)呢?《Unix高級(jí)環(huán)境編程》也說(shuō)了,當(dāng)遇到大量的基礎(chǔ)計(jì)算時(shí)(如科學(xué)計(jì)算),需要自己來(lái)設(shè)置取消點(diǎn)。
ok,得益于pthread_cancel,我們很輕松的把線程可以cancel掉,可是我們的資源呢?何時(shí)釋放...
下面來(lái)看兩個(gè)pthread函數(shù)
1.void pthread_cleanup_push(void (*routine)(void *), void *arg);
2.void pthread_cleanup_pop(int execute);
這兩個(gè)函數(shù)能夠保證在 1函數(shù)調(diào)用之后,2函數(shù)調(diào)用之前的任何形式的線程結(jié)束調(diào)用向pthread_cleanup_push注冊(cè)的回調(diào)函數(shù)
另外我們還可通過(guò)下面這個(gè)函數(shù)來(lái)設(shè)置一些狀態(tài)
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 |
當(dāng)我們?cè)O(shè)置type為PTHREAD_CANCEL_ASYNCHRONOUS時(shí),線程并不會(huì)等待命中取消點(diǎn)才結(jié)束,而是立馬結(jié)束
好了,下面貼代碼:
#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"); } static void checkResults(char *string, int rc) { if (rc) { printf("Error on : %s, rc=%d", string, rc); exit(EXIT_FAILURE); } return; } 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 free is method here you can use your own method here*/ 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"); }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于Qt添加opencv和libtorch庫(kù)的問(wèn)題
這篇文章主要介紹了Qt添加opencv和libtorch庫(kù)的相關(guān)知識(shí),兩種方法一種是通過(guò)手動(dòng)添加,一種是通過(guò)qt creator添加,需要的朋友可以參考下2022-01-01C++編寫LINUX守護(hù)進(jìn)程的實(shí)現(xiàn)代碼
這篇文章主要介紹了如何使用C++實(shí)現(xiàn)LINUX守護(hù)進(jìn)程,文中代碼非常詳細(xì),供大家學(xué)習(xí)參考,感興趣的小伙伴可以了解下2020-06-06opencv圖片的任意角度旋轉(zhuǎn)實(shí)現(xiàn)示例
這篇博客將介紹如何使用OpenCV旋轉(zhuǎn)圖像任意角度,實(shí)現(xiàn)各個(gè)角度的旋轉(zhuǎn),具有一定的參考價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-06-06C++實(shí)現(xiàn)LeetCode(198.打家劫舍)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(198.打家劫舍),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08VScode+cuda編程常見(jiàn)環(huán)境問(wèn)題的解決
本文主要介紹了VScode+cuda編程常見(jiàn)環(huán)境問(wèn)題的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02C語(yǔ)言中全局?jǐn)?shù)組和局部數(shù)組的問(wèn)題
今天同學(xué)遇到一個(gè)在C語(yǔ)言中全局?jǐn)?shù)組和局部數(shù)組的問(wèn)題,卡了許久,我也沒(méi)有第一時(shí)間看出問(wèn)題,現(xiàn)在把問(wèn)題梳理一下,并給出解決方案,需要的朋友可以參考下2012-12-12C語(yǔ)言實(shí)現(xiàn)電器銷售管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)電器銷售管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06C&C++設(shè)計(jì)風(fēng)格選擇 命名規(guī)范
本文難免帶有主觀選擇傾向,但是會(huì)盡量保持客觀的態(tài)度歸納幾種主流的命名風(fēng)格,僅供參考2018-04-04