windows是一个消息驱动的系统,也是个多任务调度系统,windows中的线程分为两类,GUI线程与Worker线程,每个GUI线程会关联消息队列,当消息处理顺序不当时,则有可能造成消息死锁。
使用VS2008打开项目工程,按F5启动调试,该工具工作正常,点击退出按钮,此时会发现该工具失去了响应。按Ctrl+Alt+Break将程序中断,发现程序停在了如下位置。
可以看出,当接收到退出消息时,该函数会被调用,函数内部设置了退出事件,然后等待另一个线程退出,不过看来WaitForSingleObject并没有返回,也就是说另一个线程并没有退出,通过阅读代码得知其执行函数为CXXXXToolDLg::g_ThreadXXXX,调出线程窗口列表,并转到该线程。
查看该线程的调用栈,发现其当前停在了User32.dll!_NtUserMessageCall函数,向下追溯发现,是该线程调用了CListCtrl::GetItemCount,而在GetItemCount中又调用了SendMessage()的原因,看来问题就出现在这个SendMessage了,不过该函数为何不能返回?
查阅MSDN,对该函数的解释如下:
如果SendMessage发送消息的目标窗口是该调用线程自己产生的,那么消息处理函数会类似子程序一样立即得到调用。如果目标窗口是另一线程产生的,系统会切换到接收该消息的线程,然后调用对应的消息处理函数。在线程之间传递的消息只有当接收线程执行获取消息的代码的时候才被处理。发送消息的线程会一直阻塞,直到接收消息的线程处理完成。
这段话不是很好理解,那么就从消息机制的实现来进行分析。
对于每个windows线程,当线程刚被创建时,所有线程都不是GUI线程,只有当线程使用Windows子系统内核服务(win32k.sys)时,Windows才将线程转换为GUI线程。同时,每个GUI线程将关联一个THREADINFO结构,这个结构中包含四个消息队列。
1. Send Message Queue发送消息队列
2. Posted Message Queue登记消息队列
3. Visualized Input Queue输入消息队列
4. Reply Message Queue 响应消息队列
Sent Message Queue:该队列保存其他程序通过SendMessage给该线程发送的消息
Posted Message Queue:该队列保存其他队列通过PostMessage给该线程发送的消息
Visualized Input Queue:保存系统队列分发过来的消息,比如鼠标或者键盘的消息
Reply Message Queue:保存向窗体发送消息后的结果,比如sendMessage操作结束后,接收消息方会发送一个Reply消息给发送方的Reply队列中,以唤醒发送队列。
每个GUI程序,都有一个消息循环,不断的通过GetMessage从消息队列中取出消息,并用DispatchMessage发送给该消息的消息响应函数,实际上是DispatchMessage调用了该消息的响应函数。
对于SendMessage函数,则有如下两种情形:
1. 当发送消息的目标窗口是调用线程自己创建时:
SendMessage会直接调用该消息的响应函数,即不需要通过消息循环。
2. 当发送消息的目标窗口是另一线程创建时:
SendMessage首先将该消息挂入接收线程的发送消息队列(Send Message Queue),然后将自身阻塞,系统调度至接收线程,并且当接收线程调用操作消息队列的函数时(经过测试:发现GetMessage或PeekMessage都可以),会取出该消息,并直接调用该消息的响应函数(注意,此时并不需要DispatchMessage),当该消息处理完成后,接收线程会发送一个Reply消息给发送方的Reply队列中,以唤醒发送队列。
因此在这个程序中,当点击退出按钮时时,主线程设置退出事件,将自身阻塞,开始等待子线程退出,而子线程则使用了SendMessage向主窗口发送了消息,并只有该消息完成才会返回,此时系统会切换到主线程,但主线程也在等待子线程,没有办法去调用操作消息队列的函数,因此形成了互相等待的局面,即死锁。
改进的方法有很多,但为了不破坏程序原有结构,采用的方法是让主线程既能等待子线程退出,同时也要能处理消息。微软为了解决这一问题,提供了一个API函数,声明如下:
DWORD WINAPI MsgWaitForMultipleObjects(
_In_ DWORD nCount,
_In_ const HANDLE *pHandles,
_In_ BOOL bWaitAll,
_In_ DWORD dwMilliseconds,
_In_ DWORD dwWakeMask
);
MsgWaitForMultipleObjects()函数类似WaitForMultipleObjects(),但它会在“对象被激发”或“消息到达队列”时被唤醒而返回。因此将代码改写为如下形式:
问题得到了解决。
在上图中,可以看到新增了一个消息的处理过程,不过根据上面的描述,在这种情形下,其实只需要增加一个PeekMessage就可以处理发送线程Send过来的消息了,在此这样写是为了保证通用性,即也能处理Post队列中的消息。
另外,也可以利用SendNotifyMessage或SendMessageTimeout来避免出现死锁,在此不再赘述。