说起多线程,我们可以说起一大堆和它相关的有趣话题来,比如什么子孙线程关系,父子线程关系,线程同步异步之类的研究话题来,而我今天所说的,是关于父子线程的一些有趣现象。
首先提出一个问题,“在多线程的应用程序中,当父线程结束之后,子线程会不会退出呢?”,本文将围绕这个问题,深入分析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