learn, think, share, communication
windows的消息队列与消息循环
从最初开始学写Windows应用程序以来,都免不了和Windows消息打交道,但是事实上很长时间都没能把Windows的消息机制彻底弄清楚。本文记叙了我对Windows消息机制以及线程与消息关系的理解,因为水平所限,不免会有些错漏,希望对此有了解的同学指正。
1、Windows的消息队列与消息循环
所有创建了窗口的Windows程序,都需要运行一个消息循环,我们在无数的Windows编程书籍中都可以看到这样的经典代码:
1: while (GetMessage(&msg, hWnd, 0, 0) > 0)
2: {
3: TranslateMessage(&msg);
4: DispatchMessage(&msg);
5: }
这里的hWnd就是创建的窗口句柄,上述循环会不断的把该窗口(hWnd)相关的消息取出来,并分发到消息处理函数当中。
GetMessage函数是用来获取当前线程消息队列当中的消息的,其中的第二个参数如果传递一个窗口句柄,那么就会获取该窗口相关的消息,如果传NULL,那么会将线程消息队列中所有的消息都取出来。如果创建了多个窗口,而只对其中一个窗口句柄调用GetMessage形成消息循环,那么别的窗口都会毫无响应。
这里需要补充说明一个概念:消息队列是操作系统为每个需要处理消息的线程创建的,任何线程只要调用过与消息有关的函数(如,GetMessage,PeekMessage),操作系统就会为该线程创建消息队列。可能有人会问,那没有窗口的线程,操作系统也有必要为其创建消息队列么?这是有可能的,因为我们可以通过诸如PostThreadMessage的Api向别的线程或者本线程发送消息,如果目标线程没有消息队列,会导致这个函数返回失败。
做一个验证上述想法实验:
我们撰写类似于如下代码(所有的示例代码我都已经在Windows7系统上编写程序做过实验,这里忽略与要说明内容无关的细节并不影响对原理的理解,就不再将完整代码附上,读者感兴趣的话可以自行实验)
1: HWND hWnd1 = CreateWindow(...);
2: HWND hWnd2 = CreateWindow(...);
3: while (GetMessage(&msg, hWnd1, 0, 0) > 0)
4: {
5: TranslateMessage(&msg);
6: DispatchMessage(&msg);
7: }
上述程序创建了两个窗口,但是消息循环只传递了其中一个窗口的句柄,那么我们看到的两个窗口中,hWnd2所属的窗口会毫无响应——无法移动也无法关闭,就好象死了一样,另一个hWnd1则可以正常拖动,关闭(这是由于我们将hWnd1的消息取出来并分派处理了)。
如果我们将消息循环稍作改动,GetMessage的参数不再传递某一个窗口的句柄,而是传递NULL:
1: while (GetMessage(&msg, NULL, 0, 0) > 0)
2: {
3: TranslateMessage(&msg);
4: DispatchMessage(&msg);
5: }
则两个窗口都可以正常响应消息了。
更进一步,我们是否可以GetMessage,而不调用DispatchMessage将其分发到窗口处理函数,而是直接把Get出来的Message自行处理呢?答案是可行,但需要区分使用的场合:
1: while (GetMessage(&msg, NULL, 0, 0) > 0)
2: {
3: TranslateMessage(&msg);
4: WNDPROC fWndProc = (WNDPROC)GetWindowLong(msg.hwnd, GWL_WNDPROC);
5: fWndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
6: }
上述代码(源自winprog.org)是可以工作的。但是对于WM_TIMER类型的消息,需要回调到Timer的回调函数中,就需要另作处理了。当你创建的窗口很多的时候,或者有Timer类型消息的时候,这种做法就可能带来麻烦,但是通过尝试,可以弄明白实际上是DispatchMessage替我们回调了窗口处理函数,并没有什么神秘之处,我们也完全可以绕开DispatchMessage。
另外在某些特殊场合,这种写法则有特别的用处:比如想在一个已存在的控件(例如系统的TreeCtrl)中添加框选一系列Item的功能,则可以在收到控件中鼠标点下消息的时候,开启一个局部的GetMessage循环,在自己的局部消息循环中等候鼠标抬起的消息,并在鼠标抬起消息中检查选框的范围,并选中相应元素。
延伸:模态对话框与非模态对话框的本质区别在哪?为什么前者会将界面控制权完全接管,而后者则可以让用户继续操作其他窗口?
在MFC中调用CDialog::DoModal() 或者 在.Net中调用继承自CommonDialog的ShowDialog() 都会在该函数当中创建一个局部的消息循环,从而将上一级消息循环阻断。直到该函数返回,我们才得以继续处理上一级消息。这便是模态对话框。
而调用CWnd::Create创建出来的对话框实际上则共用了同一级的消息循环,成功创建窗口之后GetMessage(&msg, NULL, 0, 0)也会得到针对新窗口的消息,从而让新创建的窗口与原来的窗口保持并行。
对比两者,我们可以发现模态与非模态对话框的本质区别实际上就在于是否存在一个局部的消息循环,以及该消息循环是否阻断了上一级消息循环的运行。
2、线程与消息
网上经常可以搜到UI线程(User Interface Thread)和工作线程(Working Thread)的说法,认为UI线程就是创建了窗体的线程,从而操作系统会提供消息队列,而工作线程则无消息队列——我认为这种说法有欠妥当,为了查找UI线程这种说法的起源,我在MSDN当中搜索了User Interface Thread,发现这个UI线程的概念是MFC给出来的,默认情况下派生自CWinApp的类都会默认工作在User Interface Thread中,我并没有发现Windows自身提供UI线程这个概念的证据,而且也没有什么理由让我相信操作系统会针对线程是否创建了窗体而对当前线程提供什么特别对待。我倾向于认为是这个来自于MFC的概念被多数人误解了。
操作系统可能为任何线程创建消息队列,只要该线程调用了消息获取函数,甚至都不需要该线程创建任何窗口。
为了验证上述想法,我们可以做下面这样一个实验(代码出自使用PostThreadMessage在Win32线程之间传递消息一文):
其中ThreadProc是程序运行期间创建的线程,该线程默认是没有消息队列的,因此如果主线程直接以它的线程id调用PostThreadMessage,会返回FALSE,并在GetLastError中得到1444号错误——MSDN中的解释是ERROR_INVALID_THREAD_ID 1444 Invalid thread identifier。而经过调用了PeekMessage之后,ThreadProc运行的Thread已经拥有了消息队列了,之后主线程或其他线程再调用PostThreadMessage就可以正常运行了。(这里的HANDLE hStartEvent是用来保证上述顺序的同步事件。)
此外,该程序是Console版的程序,从头至尾没有创建过window,与MFC更无瓜葛,可以证实我们的想法——任何线程都可以有消息队列,Windows并没有提供什么特殊的UI线程之一说。
1: #include "stdafx.h"
2: #include <Windows.h>
3: #include <process.h>
4: #include <stdio.h>
5:
6: #define MSG_SEND_OVERTHREAD WM_USER + 100
7: HANDLE hStartEvent; // thread start event
8:
9: unsigned _stdcall ThreadProc(PVOID param)
10: {
11: MSG msg;
12: PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
13: if (!SetEvent(hStartEvent)) //set thread start event
14: return 1;
15:
16: while(true)
17: {
18: if (GetMessage(&msg,0,0,0)) //get msg from message queue
19: {
20: switch(msg.message)
21: {
22: case MSG_SEND_OVERTHREAD:
23: char * pInfo = (char *)msg.wParam;
24: printf("%s\n", pInfo);
25: delete pInfo;
26: break;
27: }
28: }
29: }
30: return 1;
31: }
32:
33: int main()
34: {
35: HANDLE hThread;
36: unsigned nThreadID;
37: char szBuf[1024];
38: //create thread start event
39: hStartEvent = ::CreateEvent(0,FALSE,FALSE,0);
40: if(hStartEvent == 0)
41: {
42: printf("create start event failed,errno:%d\n",GetLastError());
43: return 1;
44: }
45: //start thread
46: hThread = (HANDLE)_beginthreadex( NULL
47: , 0
48: , &ThreadProc
49: , NULL
50: , 0
51: , &nThreadID );
52:
53: if(hThread == 0)
54: {
55: printf("start thread failed,errno:%d\n",GetLastError());
56: CloseHandle(hStartEvent);
57: return 1;
58: }
59:
60: //wait thread start event to avoid PostThreadMessage return errno:1444
61: ::WaitForSingleObject(hStartEvent,INFINITE);
62: CloseHandle(hStartEvent);
63: int count = 0;
64: while(true)
65: {
66: char* pInfo = new char[100];
67: //create dynamic msg
68: sprintf(pInfo,"msg_%d",++count);
69: //post thread msg
70: if( !PostThreadMessage(nThreadID
71: , MSG_SEND_OVERTHREAD
72: , (WPARAM)pInfo
73: , 0))
74: {
75: printf("post message failed,errno:%d\n",GetLastError());
76: delete[] pInfo;
77: }
78: ::Sleep(1000);
79: }
80: CloseHandle(hThread);
81: return 1;
82: }
3、对上述的诸多概念做一番小节如下:线程可能拥有消息队列,也可能没有,如果线程调用过消息获取函数,操作系统会为线程准备一个消息队列,之后,其他线程便可以向此线程发送消息。
线程中可以有消息循环,消息循环将线程中的消息取出来并且进行处理——可以自行根据消息的类型进行处理,也可以交给DispatchMessage处理,该API会回调窗口类中的窗口处理函数(依据该窗口所属窗口类别WNDCLASS的不同分别回调不同的消息处理函数)。如果线程创建了窗口,那么窗口的各种响应事件全部是由消息循环以及相关处理完成的,一个消息循环可以处理很多个窗口的消息。
消息循环可以有多个,可以在上一级消息循环的某个消息的处理过程中,局部创建一个消息循环,模态对话框就是采用这种机制创建出来的。
p.s. 本文尝试了一下用Windows Live Writter来编写草稿和发布日志,发现很方便很强大 : )
March 28, 2010 - 6:10 pm
1. 其实,按照我的理解,消息队列和线程(任务)是没有必然的关联性的,mq是mq, pthread是pthread。一个线程(任务)可以去阻塞receive在某个消息队列上,也可以同时阻塞在多个消息队列上(select/overlapped io), 也可以不去接收任何消息,一个线程也可以一直循环忙自己的事,然后每循环一次去消息队列上”扫一眼“有没有新消息而不阻塞。
所以,”消息队列是操作系统为每个需要处理消息的线程创建的“,这个说法略欠妥当。
2. 模态对话框,用消息循环解释的很精髓~
3. 读完了,我有个想法,这些由界面线程响应的消息是由谁发出的?原理上,界面响应能否不依赖于界面线程?为什么?….对了,这是windows,相关的代码是不是不可见了?
March 30, 2010 - 9:08 pm
你说的线程与消息队列没有必然关联性,我觉得是有可能的,但是我现在缺乏这方面的证据来证实或者证伪我的猜想。
本文有关线程与消息的关系,我是按照我的理解来写的,并没有找到非常权威的参考。从明面上的api调用(GetMessage及PostThreadMessage)上来看,消息队列似乎是与线程相关联的,比如PostThreadMessage就需要传递一个线程id作为参数。GetMessage调用本身则已经包含了调用发出者是当前线程的语境。
最后各种窗体的消息的发出者,我认为应该是底层的设备驱动,键盘按键,鼠标移动点击,都会触发中断,操作系统会让键盘鼠标驱动把相关的数据准备好,放在一个缓冲区里,而后再由操作系统内核将这些数据包装成一个个消息,然后按照对应的目标发送给各个窗口。牵扯底层的知识,我了解的就不是很全面了,最近正在补这方面的短板:)
另外,如果有深入剖析Windows操作系统在线程与消息方面的值得推荐的书籍或者文章的话,还请不吝介绍一下:)
==============
补充一下,刚才同一位底层很熟悉的大牛请教了一下关于线程和消息队列的问题。
一个线程,凡是通过任何的api调用的途径,中间经过了win32k.sys的,都会自动检查并将线程转为GDI线程。消息队列也就应该有了。
June 10, 2010 - 10:30 pm
呃 … 这么大段的回复,我刚看到,怎么不会给我邮箱里回一份呢…
June 25, 2010 - 9:29 am
系统就创建了两个消息队列,一个system queue,一个appliation queue,你为什么说,系统会为每个线程或窗口程序都创建一个消息队列呢?怎么会有这样的结论?
June 25, 2010 - 9:56 am
谢谢你的质疑,我对Windows内核的消息队列是如何实现的并不是很熟悉,本文的结论是建立在使用者的角度观察到的现象。
在任意线程中都可以调用GetMessage获取消息,也可以用PostThreadMessage向任何线程发送消息。并且如果一个线程没有调用过GetMessage或者PeekMessage,PostThreadMessage会失败。
上述现象使我推测每个线程中都会有消息队列存在。(但这一观点并未从微软的官方文档中获得支持,我会再去做进一步查证:-) )
对windows操作系统的消息队列运作机理,如果您有更多的信息,非常希望不吝分享。
August 20, 2010 - 9:38 pm
我认为你理解的是正确的, 消息队列就是这样运作的, 但是我想可能你少介绍了PostMessage和SendMessage的区别, 我发现很多人对这2个也存在误解