如何正確終止正在執行的子執行緒

發表於2016-07-22

最近開發一些東西,執行緒數非常之多,當使用者輸入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。當然我們同時也可以更改獲取某個訊號的行為,以此來達到我們終止子執行緒的目的。

執行輸出為:

我們可以通過截獲的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高階環境程式設計》的執行緒部分)

輸出:

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時,執行緒並不會等待命中取消點才結束,而是立馬結束

好了,下面貼程式碼:

相關文章