一、简介
本文对Windows的窗口消息进行分析,主要梳理消息的发出及消息的响应,以及跨线程边界情况下消息的响应过程进行分析。对于Windows的消息循环的透彻理解是Qt信号槽深入理解的基础。
二、以API切入进行分析
1、SendMessage
SendMessage
实现为指定的窗口发送消息,被指定的窗口于窗口过程函数接受消息后,进行特定的处理。窗口过程的函数类似如下:
MainWindow * g_mainWnd = nullptr;
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_CREATE:
// 初始化窗口
break;
case WM_COPYDATA:
{
COPYDATASTRUCT* cds = (COPYDATASTRUCT*)lParam;
QByteArray utf8Bytes((const char*)cds->lpData, cds->cbData);
QString str = QString::fromUtf8(utf8Bytes);
qDebug() << "String received:" << str;
if (g_mainWnd)
{
g_mainWnd->updateMsg(str);
}
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
参数WPARAM 和 LPARAM 两个附加参数,是long 型的,所以只能传递数字。如果想要传递字符串之类的则需要使用指针,即字符串的地址。如果指定的窗口是由调用线程创建的,则窗口过程(WndProc函数)立即作为子函数调用。如果指定的窗口是由不同线程创建的,则系统切换到该线程并调用恰当的窗口程序。线程间的消息只有在线程执行消息检索代码时才被处理。发送线程被阻塞直到接收线程处理完消息为止。
2、PostMessage
与SendMessage
不同的是,Post是将一个消息寄送到创建窗口的线程的消息队列后立即返回。消息队列的消息什么时候被处理取决于何时接受消息的线程何时GetMessage
。
3、阻塞与非阻塞(同步与异步)
SendMessage给指定的窗口发出消息后只有当消息被处理后才会继续往下执行,所以是阻塞的。这就引出两个思考:
(1) 如果接受消息的窗口处于同一个线程,系统是如何调度的?
答:同一个线程,自然就是同一个进程空间。对于全局变量、堆内存,指针指向的内存即使切换到不同的窗口过程(wndproc)访问仍然是有效的。系统的实现就很简单等价于函数的嵌套,也就是父子函数的调用。如下示意图:
(2) 如果接受消息的窗口处于另一个线程,甚至是另一个进程的时候,系统是如何调度的?
多个线程就需要了解下线程的状态切换了。简单来说一个线程有就绪态、运行态、挂起、结束。打开任务管理器可以看到很多进程在运行,一个进程至少有个主线程,自然系统中就有很多线程在”同时“运行。站在操作系统的角度,我们可以在任务管理器看到的信息,例如线程的状态、资源占用、进程和线程的关系等等,都是被操作系统操控着。所以先搞清楚的是:线程的上下文切换这个动作是谁做的?答:操作系统,就是OS软件,只不过是个特殊的软件。
那么为啥要切换上下文?首先,在单核单线程处理器时代,操作系统要让多个线程有时间片执行,就必须"雨露均沾",既快速的执行下A线程,然后快速的执行下B线程…制造一种多线程并发运行的假象。线程上下文的切换是通过时钟中断触发,且OS根据一些优先级标记决定是否要切出。线程上下文的切换说白了就是让这个CPU核干啥活。举个例子,你的右手一会写字,一会画画,但是从写字切换到画画,大脑是需要有个切换,比如切换画画的上一笔是什么,下一笔怎么画等等,对应到CPU的上下文切换就是计算机的寄存器、程序计数器等信息。
回到问题,跨线程传递消息时系统是如何调度的?
对于SendMessage
,发出消息的线程,在发出消息后就进入阻塞状态了,系统会给接收消息的线程的消息放入一条消息。然后发消息的线程会挂起,等到接受消息的线程处理完消息后,发消息的线程才继续往下走。接受消息的线程何时处理消息,取决于自身的消息循环是怎么实现的。如下是一个简单的消息循环:
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
DispatchMessage
会将消息派遣到对应的窗口过程进行消息的响应。
如果处理器是单核单线程的,那么接受消息的线程只有等到时间片后才会处理消息,如果是多线程的,也是等到时间片后才会处理消息。但相对单线程的CPU接收消息的线程可能正好就在时间片内,不需要等。
三、总结
至此,关于Windows窗口消息的发出到接收,跨线程边界的传输也进行了梳理。抛出个疑问:即为Windows的窗口循环需要自己实现,不能系统直接调对应的窗口过程?