一、消息队列

首先我们要明确一个观点:窗口是和线程相关联的,消息队列也是和线程相关联的,这个线程无论是主线程还是子线程

当一个线程被创建时,系统假定该线程不会被用于任何与用户界面相关的任务,所以不会为它分配相应的资源(如消息队列等),因为这样可以减少线程对系统资源的占用。

但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些额外的资源,以便它能够执行与用户界面有关的任务。特别是,系统会分配一个THREADINFO结构,并将这个数据结构与线程关联起来。

THREADINFO结构是微软内部的、没有被公开的数据结构,我们无法找到这个结构体的准确的定义。但从其他文档中可以得知,THREADINFO结构包含:

  • 一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。
  • 登记消息队列(posted-message queue)
  • 发送消息队列( send-message queue)
  • 应答消息队列( reply -message queue)
  • 虚拟输入队列(virtualized-input queue)
  • 唤醒标志(wake flag)
  • 用来描述线程局部输入状态的若干变量。

上图描述了THREADINFO结构中的各个成员。线程拥有了THREADINFO结构也就有了各种消息队列。

二、窗口消息处理函数

下面是一个完整的、简单的创建Windows窗体的C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hwnd, message, wParam, lParam);
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpfnWndProc = WndProc; //设置窗体消息处理函数
wndclass.lpszClassName = TEXT("SimpleWindow");
if (!RegisterClass(&wndclass)) { //注册窗体类
return 0;
};

HWND hwnd = CreateWindow(TEXT("SimpleWindow"), // window class name
TEXT("SimpleWindow"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,// initial x position
CW_USEDEFAULT,// initial y position
CW_USEDEFAULT,// initial x size
CW_USEDEFAULT,// initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL);

ShowWindow(hwnd, SW_SHOW);

MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg.wParam;
}

上面代码中的WndProc函数就是窗口消息处理函数。消息循环中的DispatchMessage函数派发消息时,系统就会调用这个函数对消息进行处理。

三、消息循环

消息循环是程序员自己编写的从线程消息队列中循环获取(Get或Peek)消息的循环体,代码大致如下:

1
2
3
4
5
while(GetMessage(&Msg, NULL, 0, 0))   
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}

3.1 循环何时结束?

GetMessage函数的返回值如下:

1
2
3
收到WM_QUIT消息,返回0
收到非WM_QUIT消息,返回非0
错误,返回-1

利用GetMessage函数返回值的特性,在收到WM_QUIT消息之后,消息循环就会结束。

3.2 TranslateMessage

TranslateMessage函数的作用就是将虚拟键值信息转换为字符信息。这一步并不是必须的。

3.3 DispatchMessage

将消息派发到窗口的消息处理函数(如第2节中的WndProc函数)。

四、PostMessage

4.1 PostMessage原理

1
2
3
4
5
BOOL PostMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);

当一个线程调用这个函数时,系统要确定是哪一个线程建立了用hwnd参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且,这个函数还设置QS_POSTMESSAGE唤醒位。PostMessage函数在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。实际上,有可能这个指定的窗口永远不会收到登记的消息。

4.2 PostThreadMessage

还可以通过调用PostThreadMessage将消息放置在线程的登记消息队列中。

1
2
3
4
5
BOOL PostThreadMessage(
DWORD dwThreadId,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);

4.3 PostQuitMessage

为了终止线程的消息循环,可以调用PostQuitMessage函数。PostQuitMessage函数类似于:
PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0);
但是,PostQuitMessage并不实际登记一个消息到任何一个THREADINFO结构的消息队列。只是在内部,PostQuitMessage会设定QS_QUIT唤醒标志,并对THREADINFO结构的nExitCode成员进行设置。因为这些操作永远不会失败,所以PostQuitMessage的原型被定义成VOID返回类型。

五、SendMessage

SendMessage的实现分为2种情况:向本线程的窗口发送消息、向其他线程的窗口发送消息。

5.1 向本线程的窗口发送消息

如果调用SendMessage的线程向本线程所建立的一个窗口发送一个消息,SendMessage的做法很简单:直接调用指定窗口的“窗口消息处理函数”,将其作为一个子例程。当“窗口消息处理函数”完成对消息的处理后,该函数会返回一个值给SendMessageSendMessage再将这个值返回给调用线程。

5.2 向其他线程的窗口发送消息

但是,当调用SendMessage的线程向其他线程所建立的窗口发送消息时,SendMessage的内部工作就复杂得多(即使两个线程在同一进程中也是如此)。

Windows要求建立窗口的线程来处理该窗口的消息。所以当一个线程调用SendMessage向一个由其他进程所建立的窗口发送一个消息,也就是向其他的线程发送消息,发送线程不可能处理窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口过程的代码和数据。实际上,发送线程要挂起,而由另外的线程处理消息。所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行下面的动作:

首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程设定QS_SENDMESSAGE标志(后面将讨论)。
其次,如果接收线程已经在执行代码并且没有等待消息(等待消息是指:如调用GetMessagePeekMessageWaitMessage等),发送的消息不会被处理,系统不能中断线程来立即处理消息。当接收进程在等待消息时,系统首先检查线程的QS_SENDMESSAGE唤醒标志是否被设定,如果是,系统扫描发送消息队列中消息的列表,并找到第一个发送的消息。有可能在这个队列中有几个发送的消息。例如,几个线程可以同时向一个窗口分别发送消息。当发生这样的事时,系统只是将这些消息追加到接收线程的发送消息队列中。

当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息。如果在发送消息队列中再没有消息了,则QS_SENDMESSAGE唤醒标志被关闭。当接收线程处理消息的时候,调用SendMessage的线程被设置成空闲状态(idle),等待一个消息出现在它的应答消息队列中。在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中。发送线程现在被唤醒,取出包含在应答消息队列中的返回值。这个返回值就是SendMessage的返回值。这时,发送线程继续正常执行。

当一个线程等待SendMessage返回时,它基本上是处于空闲状态。但它可以执行一个任务:如果系统中另外一个线程向一个窗口发送消息,这个窗口是由这个等待SendMessage返回的线程所建立的,则系统要立即处理发送的消息。在这种情况下,系统不必等待线程去调用GetMessagePeekMessageWaitMessage

由于Windows使用上述方法处理线程之间发送的消息,所以有可能造成线程挂起。例如,当处理发送消息的线程含有错误时,会导致进入死循环。那么对于调用SendMessage的线程会发生什么事呢?它会恢复执行吗?这是否意味着一个程序中的bug会导致另一个程序挂起?答案是确实有这种可能。

利用4个函数—— SendMessageTimeOutSendMessageCallbackSendNotifyMessageReplayMessage,可以编写保护性代码防止出现这种情况。