一、消息队列
首先我们要明确一个观点:窗口是和线程相关联的,消息队列也是和线程相关联的,这个线程无论是主线程还是子线程。
当一个线程被创建时,系统假定该线程不会被用于任何与用户界面相关的任务,所以不会为它分配相应的资源(如消息队列等),因为这样可以减少线程对系统资源的占用。
但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些额外的资源,以便它能够执行与用户界面有关的任务。特别是,系统会分配一个THREADINFO
结构,并将这个数据结构与线程关联起来。
THREADINFO
结构是微软内部的、没有被公开的数据结构,我们无法找到这个结构体的准确的定义。但从其他文档中可以得知,THREADINFO
结构包含:
- 一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。
- 登记消息队列(posted-message queue)
- 发送消息队列( send-message queue)
- 应答消息队列( reply -message queue)
- 虚拟输入队列(virtualized-input queue)
- 唤醒标志(wake flag)
- 用来描述线程局部输入状态的若干变量。
上图描述了THREADINFO
结构中的各个成员。线程拥有了THREADINFO
结构也就有了各种消息队列。
二、窗口消息处理函数
下面是一个完整的、简单的创建Windows窗体的C++代码:
1 |
|
上面代码中的WndProc
函数就是窗口消息处理函数。消息循环中的DispatchMessage
函数派发消息时,系统就会调用这个函数对消息进行处理。
三、消息循环
消息循环是程序员自己编写的从线程消息队列中循环获取(Get或Peek)消息的循环体,代码大致如下:
1 | while(GetMessage(&Msg, NULL, 0, 0)) |
3.1 循环何时结束?
GetMessage
函数的返回值如下:
1 | 收到WM_QUIT消息,返回0 |
利用GetMessage
函数返回值的特性,在收到WM_QUIT
消息之后,消息循环就会结束。
3.2 TranslateMessage
TranslateMessage
函数的作用就是将虚拟键值信息转换为字符信息。这一步并不是必须的。
3.3 DispatchMessage
将消息派发到窗口的消息处理函数(如第2节中的WndProc
函数)。
四、PostMessage
4.1 PostMessage原理
1 | BOOL PostMessage( |
当一个线程调用这个函数时,系统要确定是哪一个线程建立了用hwnd
参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且,这个函数还设置QS_POSTMESSAGE
唤醒位。PostMessage
函数在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。实际上,有可能这个指定的窗口永远不会收到登记的消息。
4.2 PostThreadMessage
还可以通过调用PostThreadMessage
将消息放置在线程的登记消息队列中。
1 | BOOL PostThreadMessage( |
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
的做法很简单:直接调用指定窗口的“窗口消息处理函数”,将其作为一个子例程。当“窗口消息处理函数”完成对消息的处理后,该函数会返回一个值给SendMessage
,SendMessage
再将这个值返回给调用线程。
5.2 向其他线程的窗口发送消息
但是,当调用SendMessage
的线程向其他线程所建立的窗口发送消息时,SendMessage
的内部工作就复杂得多(即使两个线程在同一进程中也是如此)。
Windows要求建立窗口的线程来处理该窗口的消息。所以当一个线程调用SendMessage
向一个由其他进程所建立的窗口发送一个消息,也就是向其他的线程发送消息,发送线程不可能处理窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口过程的代码和数据。实际上,发送线程要挂起,而由另外的线程处理消息。所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行下面的动作:
首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程设定QS_SENDMESSAGE
标志(后面将讨论)。
其次,如果接收线程已经在执行代码并且没有等待消息(等待消息是指:如调用GetMessage
、PeekMessage
或WaitMessage
等),发送的消息不会被处理,系统不能中断线程来立即处理消息。当接收进程在等待消息时,系统首先检查线程的QS_SENDMESSAGE
唤醒标志是否被设定,如果是,系统扫描发送消息队列中消息的列表,并找到第一个发送的消息。有可能在这个队列中有几个发送的消息。例如,几个线程可以同时向一个窗口分别发送消息。当发生这样的事时,系统只是将这些消息追加到接收线程的发送消息队列中。
当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息。如果在发送消息队列中再没有消息了,则QS_SENDMESSAGE
唤醒标志被关闭。当接收线程处理消息的时候,调用SendMessage
的线程被设置成空闲状态(idle),等待一个消息出现在它的应答消息队列中。在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中。发送线程现在被唤醒,取出包含在应答消息队列中的返回值。这个返回值就是SendMessage
的返回值。这时,发送线程继续正常执行。
当一个线程等待SendMessage
返回时,它基本上是处于空闲状态。但它可以执行一个任务:如果系统中另外一个线程向一个窗口发送消息,这个窗口是由这个等待SendMessage
返回的线程所建立的,则系统要立即处理发送的消息。在这种情况下,系统不必等待线程去调用GetMessage
、PeekMessage
或WaitMessage
。
由于Windows使用上述方法处理线程之间发送的消息,所以有可能造成线程挂起。例如,当处理发送消息的线程含有错误时,会导致进入死循环。那么对于调用SendMessage
的线程会发生什么事呢?它会恢复执行吗?这是否意味着一个程序中的bug会导致另一个程序挂起?答案是确实有这种可能。
利用4个函数—— SendMessageTimeOut
、SendMessageCallback
、SendNotifyMessage
和ReplayMessage
,可以编写保护性代码防止出现这种情况。