Submitted by 李马 on 2009, March 19, 2:04 PM. 技术的角落
您可以任意转载这篇文章,但请在转载时注明原始链接和作者,谢谢。
本文链接:http://www.titilima.cn/show-544-1.html
先看一段引文,出处不甚清楚,但肯定是一本纸质出版物。
> 程序启动后就执行的那个线程称为主线程(primary thread),主线程有两
> 个特点,第一,它必须负责 GUI(Graphic User Interface)程序中的主消息循
> 环。第二,这一线程的结束(不论是因为返回或因为调用了 ExitThread())会
> 使得程序中的所有线程都被强迫结束,程序也因此而结束。其他线程没有机会
> 做清理工作。
我看书的时候很少字斟句酌,一般都是只了解个大概,而像这样的句子就基本属于我无视的内容。不过,在一个偶然的机会,我注意到了这句话,并有了以下这篇评论。
先说结论:句中所提到的“主线程的两个特点”,基本不靠谱。
> 第一,它必须负责 GUI(Graphic User Interface)程序中的主消息循环。
之所以说这句话不靠谱,是因为它过度地限制了主线程的功能。在我的印象中,灵图天行者 4.0 以前的版本(含)中,主线程是一个调度器,用于调度其余的工作线程,而 UI 线程则亦是这些工作线程中的一个。换句话说,主消息循环可以不在主线程之中。
> 第二,这一线程的结束(不论是因为返回或因为调用了 ExitThread())会使得程序中的所有线程都被强迫结束,程序也因此而结束。
这句话半对半错,因为如果在主线程中调用 ExitThread 的话,其它线程是不会退出的,因此程序也不会结束。考虑以下代码:
- #include <Windows.h>
- DWORD WINAPI ThreadProc(PVOID param)
- {
- for (;;)
- {
- Sleep(1000);
- }
- return 0;
- }
- int main(void)
- {
- HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
- Sleep(1000);
- ExitThread(0);
- return 0;
- }
程序运行后,main 所在的线程被 ExitThread 结束,但 ThreadProc 的线程却会一直运行。对于这个问题,我在《Windows 编程札记》中的一段话能够很好的解释,如下。
---------- 传说中的分隔线 ----------
那么,就让我们来探索一下进程正常结束的过程吧。为了避免一切不必要代码的干扰,我选择了一个空无一物的骨架程序。
- #include <Windows.h>
- int WINAPI WinMain(
- HINSTANCE hInstance,
- HINSTANCE hPrevInstance,
- LPSTR lpCmdLine,
- int nShowCmd)
- {
- return 0;
- }
如你所见,这个程序实在是无愧于“骨架”这个称号——那真是除了骨头就是架子了。
从表面上来看,这个 WinMain 就是程序的入口。当它返回(return 0;)之后,我们的进程就结束了。当然,事实肯定没有看起来的这么简单,因为编译器是很乐于偷偷做好事的,而且做了好事还有拒不留名的习惯。
现在让我们编译这段代码,并使用 WinDbg 来调试这个程序。我们不设置任何断点,直接让程序运行到结束。在这个时侯,程序的调用堆栈信息会是下面这个样子:
- 0:000> k
- ChildEBP RetAddr
- 0012fdc4 7c92e89a ntdll!KiFastSystemCallRet
- 0012fdc8 7c81ca3e ntdll!ZwTerminateProcess+0xc
- 0012fec4 7c81ca96 kernel32!_ExitProcess+0x62
- 0012fed8 004012a1 kernel32!ExitProcess+0x14
- 0012fee4 0040148d skeleton!__crtExitProcess+0x17
- 0012ff28 004014b7 skeleton!doexit+0x113
- 0012ff3c 0040114f skeleton!exit+0x11
- 0012ffc0 7c816ff7 skeleton!__tmainCRTStartup+0x121
- 0012fff0 00000000 kernel32!BaseProcessStart+0x23
真相终于大白于天下。很显然,编译器把 kernel32.dll 的 ExitProcess API 藏到了 WinMain 的后面,使得我们的 skeleton.exe 进程最终得以退出。如果你对如何封装 ExitProcess 的细节感兴趣的话,那么可以深入研究 Visual Studio 附带的 C runtime 源代码中的 crt0.c 文件,这里就不再多介绍了。
---------- 传说中的分隔线 ----------
显而易见,ExitThread 的执行使得线程结束,也就是直接跳过了 CRT 之中的 ExitProcess,这样一来进程当然无法结束了。