說起多線程,我們可以說起一大堆和它相關的有趣話題來,比如什么子孫線程關系,父子線程關系,線程同步異步之類的研究話題來,而我今天所說的,是關於父子線程的一些有趣現象。
首先提出一個問題,“在多線程的應用程序中,當父線程結束之后,子線程會不會退出呢?”,本文將圍繞這個問題,深入分析windows中父子線程的生命周期及他們之間的關系。
我們知道,不管你使用的是何種編程語言,但當我們在windows下編程,且使用了平台相關性的庫的時候,不管我們使用什么函數來創建線程,最終的結果是,我們的代碼中都會調用CreateThread來創建線程,當然,這個工作是由你所使用的庫封裝完成的,你可以不用關心它是如何工作的。而在線程的使命完成之后,必須結束的時候,我們的代碼中又會調用ExitThread或是TeriminateThread來終止線程運行,其中,前一個函數一般用來終止自己,后一個函數可以終止任何線程,大多被用來終止其它線程。
在《Windows 核心編程》中告訴我們,一般情況下,盡量讓線程自己返回而不要使用ExitThread或是TeriminateThread來強制終止線程,也不要讓包含線程的進程在線程結束前就終止的方式來結束線程。為啥呢?
一般情況下,對於線程函數的正常返回,這才是最好的處理方式,線程函數正常返回,會處理下面4件事情:
1.線程函數中創建的所有C++對象都通過其析構函數被正常析構。
2.操作系統正確釋放線程棧使用的內存。
3.操作系統把線程的退出代碼設為線程函數的返回值。
4.系統減少線程內核對象的使用計數。
在線程終止的時候,會處理下面幾件事情:
1.線程擁有的所有用戶對象句柄會被釋放。在Windows中,大多對象都是包含了“創建這些對象的線程”的進程擁有。但一個線程有2個用戶對象:窗口和鈎子。一個線程終止時,系統會自動銷毀由線程創建或安裝的任何窗口,並卸載由線程創建或安裝的任何鈎子。其他對象只有在擁有線程的進程終止的時候才會被銷毀。
2.線程的退出代碼從STILL_ACTIVE變成傳給ExitThread或TerminateThread的代碼
3.線程內核對象的狀態變為觸發狀態。
4.如果線程是進程的最后一個活動進程,系統認為進程也終止了。
5.線程內核對象的使用計數減為1.
所以說,為了回收各種線程占用的資源,讓線程函數正常返回才是最好的解決方案。
下面解釋父子線程。對於父子線程,有兩種情況:
第一種是父線程是進程的主線程,子線程由主線程創建;
第二種情況是父線程為進程主線程創建的一個子線程,而這個子線程又創建了一個孫線程,這種情況大多被稱為子孫線程。
對於我們前面提出的問題,我們先看第二種情況,假設主線程為A,創建了線程B,然后B又創建了線程C,現在,如果線程B終止了,那么線程C會不會也終止呢?先來看看CreateThread的原型:
HANDLE WINAPI CreateThread(
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId
);
這里,我們關心的是前兩個參數,lpThreadAttributes表示的是線程內核對象的默認安全屬性,一般傳入NULL,表示具有系統默認的安全屬性,如果想要使子線程繼承這個線程的對象句柄,必須指定這個結構。對於具體如何指定在這里不在細談,我們想要知道的是C線程如果繼承了B線程的對象句柄,那么B線程結束,C線程是否也會結束?我們由操作系統對內核對象的管理得知,如果C線程繼承了B線程的對象句柄,那么這個對象引用計數會增加,而當B線程終止的時候,這個計數會遞減,但是要注意,這里並不是清零,而是遞減,操作系統對於資源的回收是看內核對象引用計數的,如果不為零,則不會回收,所以,即使C線程繼承了B線程的對象句柄,那么B線程結束,C線程還是存活的。
再看第二個參數dwStackSize,它表示的是指定線程棧使用的地址空間大小,當我們注意到,這個地址空間的分配不是在父線程之上,而是在系統物理存儲之上的時候,我們已經明白,這個空間是和父線程無關系的。
由上面兩條可得,由於我們創建的C線程沒有使用B線程的任何資源,也就是說,B線程創建的C線程,在創建之后,這兩者就是相互獨立的,所以這種情況下,如果B線程終止,那么C線程在線程函數沒有返回的時候,是不會結束的。
下面再來討論第一種情況:父線程是進程的主線程,子線程由主線程創建。這種情況比較復雜。據《深入解析Windows 操作系統》所述,Windows本身在系統級上根本就沒有進程的概念,察看這部分源代碼會發現,CreateProcess實際上調用的就是CreateThread。這說明主線程即可以概念上的進程了。至於地址空間,僅僅是一個數據結構所決定的,還是與我們所了解的進程概念無關。而《Windows 核心編程》又雲,當主線程的進入點函數(WinMain、wWinMain、main或wmain)返回時,它將返回給C/C++運行期啟動代碼,它能正確地清除該進程使用的所有的C運行期資源。當C運行期資源被 釋放之后,C運行期啟動代碼就顯式調用ExitProcess,並將進入點函數返回的值傳遞給它。這解釋了為什么只需要主線程的進入點函數返回,就能夠終止整個進程的運行。請注意,進程中運行的任何其他線程都隨着進程而一起終止運行。
MSDN文檔中聲明,進程要等到所有線程終止運行之后才終止運行。就操作系統而言,這種說法是對的。但是,C/C++運行期對應用程序采用了不同的規則,通過調用 ExitProcess,使得C/C++運行期啟動代碼能夠確保主線程從它的進入點函數返回時,進程便終止運行,而不管進程中是否還有其他線程在運行。不過,如果在進入點函數中調用ExitThread,而不是調用ExitProcess或者僅僅是返回,那么應用程序的主線程將停止運行,但是,如果進程中至少有一個線程還在運行,該進程將不會終止運行。
還有一種特殊的情況,如果子進程內發生死鎖,那么這個子進程就無法退出,也會導致整個進程都無法退出。這種就是為什么有時候我們的程序已經退出(至少界面已經關閉),但任務管理器中卻還有這個應用程序的進程存在的原因。
搞清楚了問題的答案,終於可以松口氣了,小問題,大學問,值得探討!
參考資料:MSDN《深入解析Windows 操作系統》《Windows 核心編程》
http://blog.csdn.net/blpluto/article/details/5953206
