Windows 消息以及消息处理算法--线程和消息队列详解
Windows以消息驱动的方式,使得线程能够通过处理消息来响应外界。
Windows 为每个需要接受消息和处理消息的线程建立消息队列(包括发送消息队列,登记消息队列,输入消息队列,响应消息队列),其中发送消息队列保存其他线程通过SendMessage发送给该线程建立窗口的消息,登记消息队列保存通过PostMessage发送给该线程或者该线程建立窗口的消息,输入消息队列保存系统的输入(包括键盘,鼠标输入),响应消息队列包含该线程调用SendMessage给指定窗口的窗口函数处理完后通知该线程的信息。
线程和消息队列关系:
RIT+SHIQ构成了系统的硬件输入模型的核心
系统为每个线程维护一个消息队列,还维护一个全局的消息队列,称为系统硬件输入队列(SHIQ:SystemHanrwareInputQueue),用于存储系统中硬件出发的消息。(如鼠标、键盘等引发的消息)在系统初始化的时候会建立一个特殊的线程------原始输入线程(RIT:RawInputThread).
系统为线程建立消息队列,实际上就是分配一个THREADINFO结构的数据,使其与线程关联。
在THREADINFO结构中包含有登记消息队列的指针、虚拟输入队列指针、发送队列指针、应答消息队列指针、退出代码、唤醒标记和局部输入状态变量等信息。
* 虚拟输入队列指针(Virtualized-input):接收接盘的等虚拟输入信息队列 (就是一个指针数组)
* 登记消息队列指针(Posted-Message):使用PostMessage函数发送的消息,将存放于此 (就是一个指针数组)
* 发送消息队列指针(Send-Message):SendMessage函数发送的消息存放位置 (就是一个指针数组)
* 应答消息队列指针(Reply-Message):使用SendMessage函数发送信息后,返回的信息存放于此
* nExitCode:确定线程退出状态,是一个int型,不同数值说明线程处于不同状态
* 唤醒标志:判断是否处于唤醒状态
* 局部输入状态变量:不详
PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam):(SendMessage和PostMessage的原理差不多,只是SendMessage会等待窗口过程处理完了以后才返回)
在调用该函数时,系统首先确定是哪个线程创建了hWnd参数标识的窗口,然后系统分配一块内存区域,将消息信息(该函数的实参)存储在这块区域中,并将该区域的首地址添加到线程的登记消息队列中。
整体流程:(以鼠标消息为例WM_MOUSEMOVE)
1.用户移动鼠标产生事件,系统通过设备驱动程序将消息(系统先将鼠标事件封装成消息,即MSG结构体)放入SHIQ(就是把一个指向MSG结构体的32位地址放入虚拟输入队列指针中),此时RIT会唤醒,RIT通过当前鼠标光标之下的窗口,获取创建窗口的线程ID(通过GetWindowThreadProcessId函数实现),然后将鼠标消息放入到线程的虚拟输入队列中(就是把一个指向MSG结构体的32位地址放入虚拟输入队列指针中)
2.应用程序从虚拟输入队列指针中取消息,并将其回传给操作系统
3.由操作系统调用窗口过程(通过hWnd找到所属的窗口类,在窗口类中找到窗口过程的地址)
伪算法:
Windows通过QS_SENDMESSAGE、QS_POSTMESSAGE、QS_QUIT、QS_INPUT、QS_PAINT、QS_TIMER表示是否有发送消息、登记消息、退出消息、输入消息、重绘消息、定时消息。
消息的优先级是QS_SENDMESSAGE > QS_POSTMESSAGE > QS_QUIT > QS_INPUT > QS_PAINT > QS_TIMER。
Windows处理消息的方式大概是这样的:
消息循环伪算法:
BOOL bRet = FALSE;
MSG msg;
while ((bRet = GetMessage(&msg, NULL, 0, 0))) {
if (bRet == -1) break; // On Error exit the loop
TranslateMessage(&msg); //转换消息
DispatchMessage(&msg); //发送消息,其实就是调用指定窗口的窗口函数
}
GetMessage伪算法如下:
BOOL GetMessage(MSG *lpMsg, HWND hWnd , UINT wMsgFilterMin, UINT wMsgFilterMax)
{
//查看QS_SENDMESSAGE标志,如果有的话循环处理,直到没有消息位置
DWORD dwRetVal = 0;
ThreadInfo threadInfo;
FLAG_SENDPROCLOOP:
GetThreadInfo(GetCurrentThreadId(), &threadInfo);
while (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) {
//从发送消息队列中获取消息
dwReturnVal = GetMsgFromQueue(QUEUE_SEND, lpMsg, hWnd,wMsgFilterMin, wMsgFilterMax);
//判断是否取到消息,有则调用窗口函数,无则复为QS_SENDMESSAGE标志
If (dwReturnVal == GETMESSAGE_HASMESSAGE) {
//调用指定窗口的窗口函数
CallWindowProc(hWnd, &threadInfo, lpMsg);
}
else {
QS_SENDMESSAGE = QS_SIGNALRESET;
break;
}
}
//在继续处理之前再次检查发送消息队列
if (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) goto FLAG_SENDPROCLOOP;
//检查发送消息队列, 如果有消息则取发送消息
//判断是否还有发送消息,没有了则复位QS_POSTMESSAGE标志
if (threadInfo.QS_POSTMESSAGE == QS_SIGNALSET) {
dwReturnVal = GetMsgFromQueue(QUEUE_POST, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
if (dwReturnVal == GETMESSAGE_LASTMESSAGE)
threadInfo.QS_POSTMESSAGE = QS_SIGNALRESET;
return TRUE;
}
//如果退出标志被置位
if (threadInfo.QS_QUIT == QS_SIGNALSET) {
threadInfo.QS_QUIT = QS_SIGNALRESET;
FillMessage(lpMsg, MESSAGE_QUIT);
return FALSE;
}
//检查输入消息队列
if (threadInfo.QS_INPUT == QS_SIGNALSET) {
DWORD dwRetVal = GetMessageFromQueue(QUEUE_INPUT, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
//检查是否有键盘,鼠标消息
if (Test(dwRetVal, QS_KEY) == QS_LASTMOUSEKEYMESSAGE)
threadInfo.QS_KEY = QS_SIGNALRESET;
if (Test(dwRetVal, QS_MOUSEBUTTON) == QS_LASTMOUSEMESSAGE)
threadInfo.QS_MOUSEBUTTON = QS_SIGNALRESET;
return TRUE;
}
//测试QS_PAINT
if (threadInfo.QS_PAINT == QS_SIGNALSET) {
//填充MSG,如果没有窗口过程确认窗口,则复位QS_PAINT标志
//...
//返回TRUE
threadInfo.QS_PAINT = QS_SIGNALRESET;
return TRUE;
}
if (threadInfo.QS_TIMER == QS_SIGNALSET) {
//填充MSG,如果没有定时器报时,则复位QS_TIMER标志
//...
//返回TRUE
return TRUE;
}
//等待有消息到达
dwRetVal = MsgWaitForMultipleObjectsEx(...);
if (...)
goto FLAG_SENDPROCLOOP;
//等待失败
return FALSE;
}
上面要注意的是各种消息被处理的优先级顺序,在发送队列中有发送消息时,GetMessage不返回,直到将发送队列中消息处理完毕为止,然后复位QS_SENDMESSAGE,没有发送消息时,GetMessage才查看登记消息,如果没有登记消息,则依着优先级从高到低的顺序依次处理各种消息。 如果此过程中发现了优先级低的消息,则GetMessage填充一个MSG,然后返回。如果是QS_QUIT被置位,则GetMessage返回FALSE,否则返回TRUE。 当GetMessage返回FALSE时,消息循环也就结束了。看消息循环可知,当消息循环再次调用GetMessage时,依然按照优先级顺序依次处理各种消息。请注意SendMessage发送到目标线程消息队列的消息在目标线程调用GetMessage时被处理掉,直到没有发送消息为止GetMessage才回去查询其他消息,如果有消息GetMessage取到消息返回,否则GetMessage使得线程陷入IDLE状态,被挂起,当有消息到达线程时GetMessage被唤醒,获取消息返回。