现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

对话框中WaitForSingleObject等待线程退出导致程序阻塞的原因及解决

2014-03-27 21:11 工业·编程 ⁄ 共 4451字 ⁄ 字号 暂无评论

    今天在调试程序中发现了程序中出现的一个问题,具体如下:

      在对话框中新建一个线程worker thread,当用户点击cancel时,通知该线程函数退出,同时用WaitForSingleObject等待该线程结束。但是每当用户点击Cancel后,程序会卡在OnCancel函数中的WaitForSingleObject处,必须要强制结束才能退出。

      在网上查了一下,大致原因如下:

WaitForSingleObject会阻塞对话框线程(Dialog thread),同时也会导致了对话框的消息循环机制被阻塞 ,而我在线程函数中会对对话框有一些UI操作(SetPos, SetWindowText),这些对对话框的UI操作实际上是通过线程向控件发送消息得到的 ( SendMessage(m_hWnd, PBM_SETPOS, nPos, 0L) ),因此WaitForSingleObject阻塞了消息循环,也就导致 SendMessage无法返回,卡住了线程 ,WaitForSingleObject也就无法返回了。

具体的修改方法如下:

     使用 MsgWaitForMultipleObjects代替 WaitForSingleObject, 这个函数即可以等待信号(thread,event,mutex等等),也可以等待指定类型的消息(MSG),函数声明如下:

       DWORD WINAPI MsgWaitForMultipleObjects(

        __in                         DWORD nCount, //number of objects to be wait

        __in                         const HANDLE* pHandles, //An array of object handles.

        __in                         BOOL bWaitAll, //If this parameter is TRUE, the function returns when the states

                                                                //of all objects in the pHandles array have been set to signaled

                                                                //and an message has been received.

        __in                         DWORD dwMilliseconds, //time-out interva

        __in                         DWORD dwWakeMask //the type of message to be wait

      );

参数dwWakeMask定义了要等待的消息的类型,根据这个函数,将WaitForSingleObject改写为:

DWORD dwRet = 0;
MSG msg;
while (TRUE)
{
  //wait for m_hThread to be over,and wait for
  //QS_ALLINPUT(Any message is in the queue)
  dwRet = MsgWaitForMultipleObjects (1, &m_hThread,   FALSE, INFINITE, QS_ALLINPUT);
  switch(dwRet)
  {
  case WAIT_OBJECT_0:
    break; //break the loop
  case WAIT_OBJECT_0 + 1:
    //get the message from Queue
    //and dispatch it to specific window
    PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
    DispatchMessage(&msg);
    continue;
  default:
    break; // unexpected failure
  }
  break;
}

   其中, PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)和DispatchMessage(&msg)这两句不能少,如果缺少程序仍然会卡在UI操作(SetPos, SetWindowText)处,由此判定MsgWaitForMultipleObjects实际上也阻塞了消息循环。那么,PeekMessage和 DispatchMessage是怎样工作,使得消息循环可以正常工作?对此,我有以下两点猜测:

     猜测1: PeekMessage和 DispatchMessage将SetPos中发送的 PBM_SETPOS 消息从队列中取出并且分发给相应的窗口,即完成了消息处理。为了验证这个方法,我跟踪了一下PeekMessage得到的msg,发现并没有 PBM_SETPOS 消息。这是为什么呢?这是由于SetPos是调用的SendMessage来更新进度条,因此该消息并不进入对话框线程的Message Queue中,所以PeekMessage 并不能得到PBM_SETPOS消息,也就是说被阻塞的 PBM_SETPOS消息并不是通过PeekMessage和 DispatchMessage来处理的。

     猜测2: MsgWaitForMultipleObjects函数返回后,由于不再阻塞消息处理,此时在 PeekMessage同时,消息处理函数处理了 PBM_SETPOS消息。如果是这样的话,我将 PeekMessage和 DispatchMessage改为其它的程序段,比如一些无意义的赋值语句,是否也可以达到相同的效果?实验后发现并不可行。

    用debug跟踪了一下程序执行的流程,如下:

SetPos(SendMessage) worker thread

      --> MsgWaitForMultipleObjects返回 Dlg thread

      --> PeekMessage                                         Dlg thread

--> SetPos返回 Worker thread

      --> PeekMessage返回                                   Dlg threa

根据上面的流程,合理的解释应该是:MsgWaitForMultipleObject等待过程中Worker thread仍然被阻塞,当Dialog中有消息到来时MsgWaitForMultipleObject返回,此时调用PeekMessage,消息处理函数被激活,处理 PBM_SETPOS消息,workder thread中的SetPos函数返回。

      然而,究竟是怎么样 PeekMessage会激活消息处理函数?我思考了很久在《windows核心编程》第26章“窗口消息”找到了答案:

对于接收消息的线程而言(本文中为 Dlg thread ),如果该线程正在执行代码,并且没有等待消息(如调用GetMessage、PeekMessage或WaitMessage)时,发送过来的消息不会被该线程处理,而发送消息的线程(本文中为worker thread)中的SendMessage函数也不会返回。

      根据上面的文字,也就是说,在执行 对话框中某个函数时,如果函数没有退出,并且没有执行 GetMessage、PeekMessage或WaitMessage函数,那么从别的线程发送来的SendMessage来的消息将不会被处理。此时再返回看看msdn中对PeekMessage的定义,可能会更加清楚:

Dispatches incoming sent messages, checks the thread message queue for a posted message, and retrieves the message (if any exist).

      During this call, the system delivers pending, nonqueued messages, that is, messages sent to windows owned by the calling thread using the SendMessage, SendMessageCallback, SendMessageTimeout, or SendNotifyMessage function. Then the first queued message that matches the specified filter is retrieved.

      也就是说,PeekMessage这个函数所做的工作不止是从队列中拿出posted message,还在此之前会先处理nonqueued messages(send message)。

      找到了send message的处理流程,也就不难解释上面程序执行的流程了。

      还可以看出:MsgWaitForMultipleObjects实际上在这其中并没有起到什么实质性的作用,只是定期返回而已,那么根据此,可以用固定时间的WaitForSingleObject来代替MsgWaitForMultipleObjects ,并且循环等待 ;同时,我们也没有必要手动DispatchMessage,只需要PeekMessage即可,需要注意的是如果删除了 DispatchMessage, 在 PeekMessage时不能将消息从队列取走。 代码如下:

DWORD dwRet = 0;
MSG msg;
while (TRUE)
{
  dwRet = WaitForSingleObject(m_hThread, 50);
  switch(dwRet)
  {
  case WAIT_OBJECT_0:
    break; //break the loop
  case WAIT_TIMEOUT :
    PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);
    continue;
  default:
    break; // unexpected failure
  }
  break;
}

经过测试,代码运行正确!

      当然,由于MsgWaitForMultipleObjects是基于消息驱动的返回,WaitForSingleObject只是普通的定时返回,而本文的情况就是由于消息阻塞造成的,所以MsgWaitForMultipleObjects比WaitForSingleObject在应用时时效性更好,如果改成WaitForSingleObject,在测试中感觉会有略为滞后,所以实际还是以MsgWaitForMultipleObjects为佳。

给我留言

留言无头像?