browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

主线程的两个特点

Posted by on 2009 年 03 月 19 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

先看一段引文,出处不甚清楚,但肯定是一本纸质出版物。

> 程序启动后就执行的那个线程称为主线程(primary thread),主线程有两
> 个特点,第一,它必须负责 GUI(Graphic User Interface)程序中的主消息循
> 环。第二,这一线程的结束(不论是因为返回或因为调用了 ExitThread())会
> 使得程序中的所有线程都被强迫结束,程序也因此而结束。其他线程没有机会
> 做清理工作。

我看书的时候很少字斟句酌,一般都是只了解个大概,而像这样的句子就基本属于我无视的内容。不过,在一个偶然的机会,我注意到了这句话,并有了以下这篇评论。

先说结论:句中所提到的“主线程的两个特点”,基本不靠谱。

> 第一,它必须负责 GUI(Graphic User Interface)程序中的主消息循环。
之所以说这句话不靠谱,是因为它过度地限制了主线程的功能。在我的印象中,灵图天行者 4.0 以前的版本(含)中,主线程是一个调度器,用于调度其余的工作线程,而 UI 线程则亦是这些工作线程中的一个。换句话说,主消息循环可以不在主线程之中。

> 第二,这一线程的结束(不论是因为返回或因为调用了 ExitThread())会使得程序中的所有线程都被强迫结束,程序也因此而结束。
这句话半对半错,因为如果在主线程中调用 ExitThread 的话,其它线程是不会退出的,因此程序也不会结束。考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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 编程札记》中的一段话能够很好的解释,如下。

———— 传说中的分隔线 ————

那么,就让我们来探索一下进程正常结束的过程吧。为了避免一切不必要代码的干扰,我选择了一个空无一物的骨架程序。

1
2
3
4
5
6
7
8
9
10
#include <Windows.h>
 
int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nShowCmd)
{
    return 0;
}

如你所见,这个程序实在是无愧于“骨架”这个称号——那真是除了骨头就是架子了。
从表面上来看,这个 WinMain 就是程序的入口。当它返回(return 0;)之后,我们的进程就结束了。当然,事实肯定没有看起来的这么简单,因为编译器是很乐于偷偷做好事的,而且做了好事还有拒不留名的习惯。
现在让我们编译这段代码,并使用 WinDbg 来调试这个程序。我们不设置任何断点,直接让程序运行到结束。在这个时侯,程序的调用堆栈信息会是下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
0:000> k
ChildEBP RetAddr
0012fdc4 7c92e89a ntdll!KiFastSystemCallRet
0012fdc8 7c81ca3e ntdll!ZwTerminateProcess+0xc
0012fec4 7c81ca96 kernel32!_ExitProcess+0×62
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,这样一来进程当然无法结束了。

订阅本站

6 Comments

  • At 2009.03.19 15:40, likunkun said:

    kernel32!BaseProcessStart+0x23也会调一个ExitProcess做最后的保证…@_@

    • At 2009.03.20 12:59, 米哈哈 said:

      很是深入啊~

      • At 2009.05.06 07:26, hyp said:

        出处Win32多线程程序设计
        简体版的P47

        • At 2009.05.21 15:17, 木头 said:

          在Windows核心编程的 p69 中提到,如果在进入点函数中调用ExitThread,而不是调用ExitProcess或者仅仅是返回,那么应用程序的主线程将停止运行,但是,如果进程中至少有一个线程中运行,该进程将不会终止运行。

          • At 2010.11.19 16:14, 匿名 said:

            在main()或WinMain()返回(return)前,程序总是先等待所有的线程结束。

            • At 2010.11.19 17:31, 李马 said:

              建议你实际测试一下,然后再下结论。

              (Required)
              (Required, will not be published)