WaitForMultipleObject与MsgWaitForMultipleObjects用法

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

WaitForMultipleObject与MsgWaitForMultipleObjects⽤法⽤户模式的线程同步机制效率⾼,如果需要考虑线程同步问题,应该⾸先考虑⽤户模式的线程同步⽅法。

但是,⽤户模式的线程同步有限制,对于多个进程之间的线程同步,⽤户模式的线程同步⽅法⽆能为⼒。

这时,只能考虑使⽤内核模式。

⽤户模式与内核模式线程同步机制⽐较
⽤户模式内核模式
优点线程同步机制速度快⽀持多个进程之间的线程同步, 防⽌死锁
缺点容易陷⼊死锁状态线程同步机制速度慢
多个进程之间的线程同步会出现问题。

线程必须从⽤户模式转为内核模式。

(⽐如竞争资源、死锁)这个转换需要很⼤的代价:
往返⼀次需要占⽤x 8 6平台上的⼤约1 00 0个C P U周期
Windows提供了许多内核对象来实现线程的同步。

对于线程同步⽽⾔,这些内核对象有两个⾮常重要的状态:
“已通知”状态,“未通知”状态(也有翻译为:受信状态,未受信状态)。

Windows提供了⼏种内核对象可以处于已通知状态和未通知状态:
进程、线程、作业、⽂件、控制台输⼊/输出/错误流、事件、等待定时器、信号量、互斥对象。

你可以通知⼀个内核对象,使之处于“已通知状态”,然后让其他等待在该内核对象上的线程继续执⾏。

你可以使⽤Windows提供的API函数,等待函数来等待某⼀个或某些内核对象变为已通知状态。

⼀、WaitForSingleObject、WaitForMulitpleObjects
函数功能: 等待⼀个内核对象变为已通知状态
可以使⽤WaitForSingleObject函数来等待⼀个内核对象变为已通知状态:
DWORD WaitForSingleObject(
HANDLE hObject, //指明⼀个内核对象的句柄
DWORD dwMilliseconds); //等待时间
该函数需要传递⼀个内核对象句柄,该句柄标识⼀个内核对象,
如果该内核对象处于未通知状态,则该函数导致线程进⼊阻塞状态;
如果该内核对象处于已通知状态,则该函数⽴即返回WAIT_OBJECT_0。

第⼆个参数指明了需要等待的时间(毫秒),可以传递INFINITE指明要⽆限期等待下去,
如果第⼆个参数为0,那么函数就测试同步对象的状态并⽴即返回。

如果等待超时,该函数返回WAIT_TIMEOUT。

如果该函数失败,返回WAIT_FAILED。

可以通过下⾯的代码来判断:
DWORD dw = WaitForSingleObject(hProcess, 5000); //等待⼀个进程结束
switch (dw)
{
case WAIT_OBJECT_0:
// hProcess所代表的进程在5秒内结束
break;
case WAIT_TIMEOUT:
// 等待时间超过5秒
break;
case WAIT_FAILED:
// 函数调⽤失败,⽐如传递了⼀个⽆效的句柄
break;
}
还可以使⽤WaitForMulitpleObjects函数来等待多个内核对象变为已通知状态:
DWORD WaitForMultipleObjects(
DWORD dwCount, //等待的内核对象个数
CONST HANDLE* phObjects, //⼀个存放被等待的内核对象句柄的数组
BOOL bWaitAll, //是否等到所有内核对象为已通知状态后才返回
DWORD dwMilliseconds); //等待时间
该函数的第⼀个参数指明等待的内核对象的个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的⼀个值。

phObjects参数是⼀个存放等待的内核对象句柄的数组。

bWaitAll参数如果为TRUE,则只有当等待的所有内核对象为已通知状态时函数才返回,
如果为FALSE,则只要⼀个内核对象为已通知状态,则该函数返回。

第四个参数和WaitForSingleObject中的dwMilliseconds参数类似。

该函数失败,返回WAIT_FAILED;
如果超时,返回WAIT_TIMEOUT;
如果bWaitAll参数为TRUE,函数成功则返回WAIT_OBJECT_0,
如果bWaitAll为FALSE,函数成功则返回值指明是哪个内核对象收到通知。

可以如下使⽤该函数:
HANDLE h[3]; //句柄数组
//三个进程句柄
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); //等待3个进程结束
switch (dw)
{
case WAIT_FAILED:
// 函数呼叫失败
break;
case WAIT_TIMEOUT:
// 超时
break;
case WAIT_OBJECT_0 + 0:
// h[0](hProcess1)所代表的进程结束
break;
case WAIT_OBJECT_0 + 1:
// h[1](hProcess2)所代表的进程结束
break;
case WAIT_OBJECT_0 + 2:
// h[2](hProcess3)所代表的进程结束
break;
}
你也可以同时通知⼀个内核对象,同时等待另⼀个内核对象,这两个操作以原⼦的⽅式进⾏:
DWORD SignalObjectAndWait(
HANDLE hObjectToSignal, //通知的内核对象
HANDLE hObjectToWaitOn, //等待的内核对象
DWORD dwMilliseconds, //等待的时间
BOOL bAlertable); //与IO完成端⼝有关的参数,暂不讨论
该函数在内部使得hObjectToSignal参数所指明的内核对象变成已通知状态,
同时等待hObjectToWaitOn参数所代表的内核对象。

dwMilliseconds参数的⽤法与WaitForSingleObject函数类似。

该函数返回如下:
WAIT_OBJECT_0,
WAIT_TIMEOUT,
WAIT_FAILED,
WAIT_IO_COMPLETION。

等你需要通知⼀个互斥内核对象并等待⼀个事件内核对象的时候,可以这么写:
ReleaseMutex(hMutex);
WaitForSingleObject(hEvent, INFINITE);
可是,这样的代码不是以原⼦的⽅式来操纵这两个内核对象。

因此,可以更改如下:SignalObjectAndWait(hMutex, hEvent, INFINITE, FALSE);
⼆、MsgWaitForMultipleObjects
函数功能:阻塞时仍可以响应消息
MsgWaitForMultipleObjects()函数类似WaitForMultipleObjects(),
但它会在“对象被激发”或“消息到达队列”时被唤醒⽽返回。

MsgWaitForMultipleObjects()多接收⼀个参数,允许指定哪些消息是观察对象。

DWORD MsgWaitForMultipleObjects(
DWORD nCount, // 表⽰pHandles所指的handles数组的元素个数,最⼤容量是MAXIMUM_WAIT_OBJECTS
LPHANDLE pHandles, // 指向⼀个由对象handles组成的数组,这些handles的类型不需要相同
BOOL fWaitAll, // 是否等待所有的handles被激发才返回
DWORD dwMilliseconds, // 超时时间
DWORD dwWakeMask // 欲观察的⽤户输⼊消息类型
);
返回值
WAIT_TIMEOUT :因时间终了⽽返回
WAIT_OBJECT_0 :当bWaitAll是TRUE
WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount – 1) :
bWaitAll是FALSE,将返回值减去WAIT_OBJECT_0,就表⽰数组中哪⼀个handle被激发了WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount – 1) :等待的对象中有任何mutexes WAIT_FAILED :函数失败时返回该值,可以使⽤GetLastError()找出失败的原因
WAIT_OBJECT_0 + nCount :消息到达队列
MsgWaitForMultipleObjects()的正确使⽤⽅式是改写主消息循环,
使得激发状态的handles得以像消息⼀样被对待。

通常程序中只会有⼀个地⽅调⽤MsgWaitForMultipleObjects(),⽽这个调⽤存在于消息循环中。

注意:
1. 在收到WM_QUIT之后,Windows仍然会传送消息给你,
如果要在收到WM_QUIT之后等待所有线程结束,必须继续处理你的消息,
否则窗⼝会变得反应迟钝,⽽且没有重绘能⼒。

2.MsgWaitForMultipleObjects()不允许handles数组中有缝隙产⽣。

所以当某个handle被激发了时,应该在下⼀次调⽤MsgWaitForMultipleObjects之前
先把handles数组做个整理、紧压,不要只是把数组中的handle设为NULL
3.如果有另⼀个线程改变了对象数组,⽽那是你正在等待的,
那么需要⼀种⽅法,可以强迫MsgWaitForMultipleObjects返回,
并重新开始,以包含新的handle
三、MsgWaitForMultipleObjectsEx
函数功能:阻塞时仍可以响应消息
函数原型
DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount, // 句柄数组中句柄数⽬
LPHANDLE pHandles, // 指向句柄数组的指针
DWORD dwMilliseconds, // 以毫秒计的超时值
DWORD dwWakeMask, // 要等待的输⼊事件类型
DWORD dwFlags // 等待标志
);
参数
nCount,指定pHandles指向的数组中的对象句柄数⽬。

最⼤对象数⽬是MAXIMUM_WAIT_OBJECTS-1
pHandles ,指向⼀个对象句柄数组。

要得到可以使⽤的对象句柄类型清单,请查看备注部分。

数组中可以包含多种对象类型。

Windows NT: 数组中句柄必须拥有SYNCHRONIZE访问权。

要得到更多相关信息,请查阅MSDN中Standard Access Rights。

dwMilliseconds ,指定以毫秒计的超时值。

即使参数dwWakeMask与dwFlags中指定的条件未满⾜,超时后函数仍然返回。

如果dwMilliseconds值为0,函数测试指定的对象状态并⽴即返回。

如果dwMilliseconds值为INFINITE,函数超时周期为⽆穷⼤。

dwWakeMask ,指定被加到对象句柄数组中的输⼊事件对象句柄的对象类型。

这个参数可以是下⾯列出值的任意组合:
值含义
QS_ALLEVENTS : WM_TIMER, WM_PAINT, WM_HOTKEY输⼊消息或登记消息(posted message)在消息队列中
QS_ALLINPUT : 任何消息在消息队列中
QS_ALLPOSTMESSAGE : 登记消息(在此处列出的除外)在消息队列中
QS_HOTKEY : WM_HOTKEY消息在消息队列中
QS_INPUT: 输⼊消息在消息队列中
QS_KEY : WM_KEYUP,WM_KEYDOWN,WM_SYSKEYUP或WM_SYSKEYDOWN消息在消息队列中
QS_MOUSE : WM_MOUSEMOVE消息或⿏标点击消息(WM_LBUTTONUP,WM_RBUTTONDOWN等)在消息队列中
QS_MOUSEBUTTON : ⿏标点击消息(WM_LBUTTONUP,WM_RBUTTONDOWN等)在消息队列中
QS_MOUSEMOVE : WM_MOUSEMOVE消息在消息队列中
QS_PAINT : WM_PAINT消息在消息队列中
QS_POSTMESSAGE : 登记消息(在此处列出的除外)在消息队列中
QS_SENDMESSAGE : 由另⼀个线程或应⽤发送的消息在消息队列中
QS_TIMER : WM_TIMER消息在消息队列中
dwFlags ,指定等待类型。

这个参数可以是下⾯列出值的任意组合:
0当对象中任意⼀个变为有信号状态则函数返回。

返回值指出是哪个对象状态的改变导致函数返回。

MWMO_WAITALL 只有当pHandles数组中所有对象有信号时函数返回
MWMO_ALERTABLE 调⽤QueueUserAPC加⼊⼀个APC将导致函数返回
MWMO_INPUTAVAILABLE 只适⽤于Windows 98, Windows NT 5.0及其以后版本:
消息队列中存在输⼊函数将返回,甚⾄于输⼊已经被另⼀个函数检测过了,如PeekMessage函数
返回值
假如函数成功,返回值表明引起函数返回的事件。

成功的函数值是下⾯中的⼀个:
值含义
WAIT_OBJECT_0 到(WAIT_OBJECT_0 + nCount - 1)
假如MWMO_WAITALL标志置位,返回值指明所有指定的对象处于有信号状态。

返回值减去WAIT_OBJECT_0就是pHandles数组中引起函数返回的对象的索引
WAIT_OBJECT_0 + nCount
有新的在dwWakeMask参数中指定的输⼊类型存在于输⼊队列中。

函数如:PeekMessage,GetMessage,GetQueueStatus与WaitMessage将队列中的消息标记为旧的。

因此,当你在这些函数之后调⽤MsgWaitForMultipleObjectsEx,函数将不会返回,除⾮有新的被指定的输⼊到达。

当⼀个需要该线程活动的系统事件发⽣时也将返回该值,例如前台活动。

因此即使没有相应的输⼊发⽣或dwWaitMask置0,MsgWaitForMultipleObjectsEx也可以返回。

如果发⽣这种情况,那么在再次调⽤MsgWaitForMultipleObjectsEx之前要调⽤PeekMessage或GetMessage处理系统事件。

WAIT_ABANDONED_0 到(WAIT_ABANDONED_0 + nCount - 1)
假如MWMO_WAITALL标志置位,返回值指明所有指定的对象处于有信号状态并且⾄少其中的⼀个是⼀个被舍弃的(abandoned)互斥对象。

另外,返回值减去WAIT_ABANDONED_0即是pHandles数组中引起函数返回的被舍弃的互斥对象的索引
WAIT_IO_COMPLETION
等待被⼀加⼊队列中的⽤户模式异步过程调⽤(user-mode asynchronous procedure call (APC))所终⽌
WAIT_TIMEOUT
超时,但dwFlags与dwWakeMask参数中条件未满⾜
假如函数调⽤失败,返回值是0xFFFFFFFF。

若想获得更多的错误信息,请调⽤GetLastError函数。

备注
MsgWaitForMultipleObjectsEx函数检测是否dwWakeMask与dwFlags参数中指定的条件满⾜。

假如条件未满⾜,调⽤线程进⼊⾼效的等待状态。

线程在等待条件之⼀满⾜或超时时只⽤很少的处理器时间。

返回前,等待函数会修改某些异步对象的状态。

修改只会针对那些置信号状态后会导致函数返回的对象,例如系统将信号对象(semaphore)的引⽤计数减⼀。

当dwFlags为零并且多个对象处于信号状态时,函数选择对象中的⼀个来确保等待;未被选中的对象的状态不受影响。

MsgWaitForMultipleObjectsEx函数可以在pHandles数组中指定下列的对象类型:
改变通知(Change notification), 控制台输⼊, 事件, 作业(job), 互斥, 进程, 信号, 线程, 等待计时器
要获取更多信息,请参阅Synchronization Objects
QS_ALLPOSTMESSAGE与QS_POSTMESSAGE标志被消除时是有区别的。

QS_POSTMESSAGE在你调⽤GetMessage或PeekMessage时被消除,⽽不管你是否正在过滤消息。

QS_ALLPOSTMESSAGE在你调⽤不过滤消息(wMsgFilterMin与wMsgFilterMax皆为零)的GetMessage或PeekMessage时被消除。

这在你调⽤PeekMessage多次以获得不同区域的消息时会很有⽤。

MsgWaitForMultipleObjectsEx复制句柄表,将消息队列事件加⼊其中,然后调⽤WaitForMultipleObjects
⽰例代码段
//⼀段代码,等待线程与事件对象及消息,超时值为2000毫秒
CWinThread* pThread = AfxBeginThread((AFX_THREADPROC)YourThreadFun, NULL);
HANDLE hThreadAndEvent[ 2 ];
hThreadAndEvent[ 0 ] = pThread->m_hThread;
hThreadAndEvent[ 1 ] = ::CreateEvent( NULL, FALSE, FALSE, NULL );
DWORD dwReturn = ::MsgWaitForMultipleObjectsEx(2, hThreadAndEvent, 2000,//2秒醒来⼀次
QS_ALLEVENTS, MWMO_INPUTAVAILABLE );
if ( dwReturn==WAIT_OBJECT_0 )
{
//线程对象通知
}
if ( dwReturn==WAIT_OBJECT_0+1 )
{
//事件对象通知
}
if ( dwReturn == WAIT_OBJECT_0+2 )
{
//消息
}
if ( dwReturn == WAIT_TIMEOUT )
{
//超时
}
多线程中,⼀般主线程创建线程(CreateThread)后,由⼯作线程函数完成具体内容,
⼯作线程在返回时通过发消息PostMessage告诉主线程结果,主线程做相当处理。

项⽬需求:
总共有多条任务要执⾏,主线程需要需要等待第1条任务执⾏结果,如果成功就往下执⾏,失败则重新发3次。

这样主线程就需要等待⼯作线程的结果了。

我们采⽤事件的⽅式,主线程调⽤完第⼀条任务后等待信号(WaitForSingleObject),
这样就会出现消息堵塞问题。

其结果总是超时。

主线程原代码:
DWORD dwRet = WaitForSingleObject(hEventOk,1000);
if(dwRet==WAIT_TIMEOUT)
//超时处理
解决⽅法:
1、使⽤MsgWaitForMultipleObjects代替,理论上可以实现,我在测试时是成功的,到具体项⽬中永远没有等到信号。

(也可以使⽤⼀个全局的BOOL变量,每次去判断这个变量的状态来检查信号,这样也可以)
BOOL bIsok = TRUE;
while(bIsok)
{
DWORD dwTime = MsgWaitForMultipleObjects(1,&hEventOk,FALSE, 10, QS_ALLINPUT);
MSG msg;
//等到信号
switch(dwTime)
{
case WAIT_OBJECT_0:
{
bIsok = FALSE;
}
break;
case WAIT_OBJECT_0+1:
{
//有消息
if(PeekMessage(&msg,NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
break;
case WAIT_TIMEOUT:
{
//超时处理
}
break;
}
}
2、使⽤WaitForSingleObject设置最⼤超时次数,同时处理消息。

(可⾏)
DWORD dwRet = 0;
MSG msg;
int nCount =0;
while (TRUE)
{
if (nCount>MAXPMSG)//最⼤次数(宏定义)
{
//超时
break;
}
dwRet = WaitForSingleObject(hEventOk,10);
switch(dwRet)
{
case WAIT_OBJECT_0: //有信号
break; //break the loop
case WAIT_TIMEOUT:
PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);//转发消息
nCount++;
continue;
default:
break; // unexpected failure
}
break;
}
3、把要处理的任务全部⽤线程处理,由线程等待每个任务是否成功。

主线程只需要调⽤⼀下⼯作线程,然后将界⾯暂时BeginWaitCursor,
当⼯作线程完成任务后,发消息告诉主线程,在⾃定义的消息中判断是否成功还是失败再EndWaitCursor吧。

使⽤多线程技术可以显著地提⾼程序性能,本⽂就讲讲在程序中如何使⽤⼯作线程,以及⼯作线程与主线程通讯的问题。

⼀创建线程
使⽤MFC提供的全局函数AfxBeginThread()即可创建⼀个⼯作线程。

线程函数的标准形式为UINT MyFunProc(LPVOID );
此函数既可以是全局函数,也可以是类的静态成员函数。

之所以必须是静态成员函数,是由于类的⾮静态成员函数,
编译器在编译时会⾃动加上⼀个this指针参数,
如果将函数设置为静态的成员函数,则可以消除this指针参数。

如果想在线程函数中任意调⽤类的成员变量(此处指的是数据成员,⽽不是控件关联的成员变量),
则可以将类的指针作为参数传递给线程函数,然后经由该指针,就可以调⽤类的成员变量了。

//线程函数,类的静态成员函数
UINT CThreadTest::TH_SetProgress(LPVOID lpVoid)
{
CThreadTest *pTest=(CThreadTest *)lpVoid;
pTest->SetProgress();
return0;
}
//类的成员函数,此函数执⾏实际的线程函数操作,却可以⾃如的调⽤成员数据
void CThreadTest::SetProgress()
{
int nCount=0;
while (1)
{
m_progress.SetPos(nCount); //设置进度条进度
// this->SendMessage(WM_SETPROGRESSPOS,nCount,0);//也可以采⽤这种⽅式设置
nCount++;
if (g_exitThread)
{
return;
}
Sleep(200);
}
}
⼆线程函数体的设计
有过多线程设计经验的⼈都有体会,多线程设计最重要的就是要处理好线程间的同步和通讯问题。

如解决不好这个问题,会给程序带来潜藏的隐患。

线程的同步可以利⽤临界区、事件、互斥体和信号量来实现,
线程间的通讯可利⽤全局变量和发消息的形式实现。

其中事件和临界区是使⽤得⽐较多的⼯具。

请看下⾯的线程函数体:
UINT AnalyseProc(LPVOID lVOID)
{
if(WAIT_OBJECT_0== WaitForSingleObject(m_eventStartAnalyse.m_hThread,INFINITE))
{
while (WAIT_OBJECT_0 == WaitForSingleObject(m_eventExitAnalyse.m_hThread,0))
{
DWORD dRet=WaitForSingleObject(m_eventPause.m_hThread,0);
if (dRet == WAIT_OBJECT_0)
{
//暂停分析
Sleep(10);
}
else if (dRet == WAIT_TIMEOUT)
{
//继续分析
//
}
}
}
return0;
}
上⾯的线程函数⽤到了三个事件变量eventStartAnalyse、eventExitAnalyse和eventPause,分别⽤来控制线程函数的启动、退出以及暂停。

再配以WaitForSingleObject函数,就可以⾃如的控制线程函数的执⾏,
这是在线程函数体内应⽤事件变量的典型⽅式,也是推荐的⽅式。

⽆论是⼯作线程还是⽤户界⾯线程,都有消息队列,
都可以接收别的线程发过来的消息也可以给别的线程发送消息。

给⼯作线程发消息使⽤的函数是PostThreadMessage()。

此函数的第⼀个参数是接收消息的线程的ID。

此函数是异步执⾏的,机制和PostMessage⼀样,
就是把消息抛出后就⽴即返回,不理会消息是否被处理完了。

这⾥还有着重强调⼀点,线程消息队列是操作系统帮我们维护的⼀种资源,
所以它的容量也是有限制的。

笔者曾经做过实验,
在5~6秒事件内调⽤PostThreadMessage往线程消息队列⾥发送5万多条消息,
可是由于线程函数处理消息的速度远慢于发送速度,
结果导致线程消息队列⾥已经堆满了消息,⽽发送端还在发消息,
最终导致消息队列溢出,很多消息都丢失了。

所以,如果你要在短时间内往线程消息队列⾥发送很多条消息,
那就要判断⼀下PostThreadMessage函数的返回值。

当消息队列已经溢出时,此函数返回⼀个错误值。

根据返回值,你就可以控制是否继续发送。

⼯作线程给主线程发消息使⽤的是SendMessage和PoseMessage函数。

这两个函数的区别在于SendMessage函数是阻塞⽅式,⽽PoseMessage函数是⾮阻塞⽅式。

如果不是严格要求⼯作线程与主线程必须同步执⾏,则推荐使⽤PoseMessage。

不要在线程函数体内操作MFC控件,因为每个线程都有⾃⼰的线程模块状态映射表,
在⼀个线程中操作另⼀个线程中创建的MFC对象,会带来意想不到的问题。

更不要在线程函数⾥,直接调⽤UpdataData()函数更新⽤户界⾯,这会导致程序直接crash。

⽽应该通过发送消息给主线程的⽅式,在主线程的消息响应函数⾥操作控件。

上⾯提到的SetProgress函数和AnalyseProc函数均为线程函数,
但它们都不能接收别的线程发过来的消息,虽然它们都可以给主线程发消息。

它们要想能够接收别的线程发过来的消息,
则必须调⽤GetMessage或PeekMessage函数。

这两个函数的主要区别在于:GetMessage函数可以从消息队列中抓取消息,
当抓取到消息后,GetMessage函数会将此条消息从消息队列中删除。

⽽且,如果消息队列中没有消息,则GetMessage函数不会返回,
CPU转⽽回去执⾏别的线程,释放控制权。

GetMessage返回的条件是抓取的消息是WM_QUIT。

PeekMessage函数也可以从消息队列中抓取消息,
如果它的最后⼀个参数设置为PM_NOREMOVE,则不从消息队列中删除此条消息,
此条消息会⼀直保留在消息队列中。

如果它的最后⼀个参数是PM_REMOVE,则会删除此条消息。

如果消息队列中没有消息,则PeekMessage函数会⽴刻返回,
⽽不是像GetMessage⼀样就那样等在那⼉。

PeekMessage函数就像是窥探⼀下消息队列,
看看有没有消息,有的话就处理,没有就离开了。

这⼀点也是两个函数的最⼤不同。

下⾯的代码演⽰了在线程函数中使⽤这两个函数的三种⽅式,
这三种⽅法可以达到同样的效果:
void CThreadTest::SetSlider()
{
// 在线程函数⾥启动⼀个时钟,每50毫秒发送⼀个WM_TIMER消息
int nTimerID=::SetTimer(NULL,1,50,NULL);
int nSliderPos=0;
MSG msg;
while (1)
{
//⽅式⼀使⽤GetMessage函数
if (::GetMessage(&msg,NULL,0,0))
{
switch(msg.message)
{
case WM_TIMER:
{
nSliderPos++;
::SendMessage(this->m_hWnd,WM_SETSLIDERPOS,nSliderPos,0);
}
break;
case WM_QUIT_THREAD: //⾃定义消息
{
::KillTimer(NULL,1);
return;
}
break;
default:
break;
}
}
//⽅式⼆使⽤PeekMessage函数
if (::PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
switch(msg.message)
{
case WM_TIMER:
{
nSliderPos++;
::SendMessage(this->m_hWnd,WM_SETSLIDERPOS,nSliderPos,0);
}
break;
case WM_QUIT_THREAD: //⾃定义消息
{
::KillTimer(NULL,1);
return;
}
break;
default:
break;
}
}
else
{
//必须有此操作,要不然当没有消息到来时,线程函数相当于陷
//⼊空循环,cpu的占有率会飙升
Sleep(20);
}
//⽅式三同时使⽤PeekMessage和GetMessage函数
if (::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
if(::GetMessage(&msg,NULL,0,0))
{
switch(msg.message)
{
case WM_TIMER:
{
nSliderPos++;
::SendMessage(this->m_hWnd,WM_SETSLIDERPOS,nSliderPos,0);
}
break;
case WM_QUIT_THREAD: //⾃定义消息
{
::KillTimer(NULL,1);
return;
}
break;
default:
break;
}
}
}
else
{
Sleep(20);
}
}
前⾯已经介绍过了,不建议线程函数⾥⽤SendMessage给主线程发消息,
因为这个函数是同步操作,就是如果SendMessage函数不执⾏完,是不会返回的,
这样线程函数就⽆法继续执⾏。

有时这种操作容易导致⼯作线程和主线程死锁,这个我们后⾯会谈到,会介绍⼀种解决⽅法。

三线程的退出
线程的退出有多种⽅式,⽐如可以调⽤TerminateThread()函数强制线程退出,
但不推荐这种⽅式,因为这样做会导致线程中的资源来不及释放。

最好的也是推荐的⽅式,是让线程函数⾃⼰退出。

就像上⾯介绍的SetProgress()函数中,⽤全局变量g_exitThread使线程退出。

⽽AnalyseProc⽤WAIT_OBJECT_0 ==WaitForSingleObject(m_eventExitAnalyse.m_hThread,0)这种⽅式来退出线程,还有在SetSlider函数中利⽤发送⾃定义消息
WM_QUIT_THREAD的⽅式令线程退出。

这些都是可以使⽤的⽅法。

当主线程要退出时,为了能保证线程的资源能全部地释放,主线程必须等待⼯作线程退出。

线程对象和进程对象⼀样,也是内核对象,⽽且线程对象的特点是当线程退出时,
线程内核对象会⾃动变为有信号状态,能够唤醒所有正在等待它的线程。

我们通常都习惯于使⽤WaitForSingleObject等函数来等待某个内核对象变为有信号状态,
但是我想说的是,在主线程中不要使⽤WaitForSingleObject和WaitForMultipleObjects两个函数等待线程退出,其原因就是有导致程序死锁的隐患,
特别是线程函数⾥调⽤了SendMessage或是直接操作了MFC对象,更易出现此种现象。

下⾯的函数是⼀个在主线程中⽤来等待SetProgress()线程函数退出的函数:
//退出线程
void CThreadTest::OnButton2()
{
g_exitThread=TRUE; //设置全局变量为真,令线程退出
#if 1
WaitForSingleObject(m_pThread1->m_hThread,INFINITE); //⽆限等待
#else
DWORD dRet;
MSG msg;
while (1)
{
dRet=::MsgWaitForMultipleObjects(1,&m_pThread1->m_hThread,FALSE,INFINITE,QS_ALLINPUT);
if (dRet == WAIT_OBJECT_0+1)
{
while (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
break;
}
}
#endif
}
在上⾯的函数中我⽤#if #else #endif这组预编译指令控制函数的执⾏代码,
如果我令#if 1,则执⾏WaitForSingleObject函数,如果我令#if 0,则执⾏DWORD dRet路径。

⾸先令#if 1,测试会发现,程序死锁了。

原因是当程序执⾏到WaitForSingleObject函数时,主线程挂起,等待线程函数退出,
此时CPU切换到线程函数体内执⾏,如果执⾏到if (g_exitThread)处,则线程函数顺利退出,可如果执⾏到m_progress.SetPos(nCount)处,由于SetPos函数是在主线程中完成的操作,Windows是基于消息的操作系统,很多操作都是靠发消息完成的,由于主线程已经挂起,
所以没有机会去消息队列中抓取消息并处理它,结果导致SetPos函数不会返回,
⼯作线程也被挂起,典型的死锁。

如果不⽤m_progress.SetPos,⽽改⽤this->SendMessage(…),其结果是⼀样的。

此时如果⽤了PostMessage,则⼯作线程会顺利退出,因为PostMessage是异步执⾏的。

由此可见,在主线程中⽤WaitForSingleObject等待⼯作线程退出是有很⼤隐患的。

为解决这⼀问题,微软特提供了⼀个MsgWaitForMultipleObjects函数,
该函数的特点是它不但可以等待内核对象,还可以等消息。

也就是当有消息到来时,该函数也⼀样可以返回,并处理消息,这样就给了⼯作线程退出的机会。

WORD MsgWaitForMultipleObjects(
DWORD nCount, //要等待的内核对象数⽬
LPHANDLE pHandles, //要等待的内核对象句柄数组指针
BOOL fWaitAll, //是等待全部对象还是单个对象
DWORD dwMilliseconds,//等待时间
DWORD dwWakeMask );//等待的消息类型
下⾯就详解⼀下该函数的参数使⽤⽅法:
DWORD nCount:要等待的内核对象的数⽬。

如果等待两个线程退出,则nCount=2;LPHANDLE pHandles:要等待的内核对象句柄数组指针。

如果只要等待⼀个线程退出,则直接设置该线程句柄的指针即可:
MsgWaitForMultipleObjects(1,&m_pThread->m_hThread,…)
如果要等待两个线程退出,则使⽤⽅法为:
HANDLE hArray[2]={ m_pThread1->m_hThread , m_pThread2->m_hThread };
MsgWaitForMultipleObjects(2,hArray,…)
BOOL fWaitAll: TRUE-表⽰只有要等待的线程全部退出后,此函数才返回,
FALSE-表⽰要等待的线程中任意⼀个退出了,或是有消息到达了,此函数均会返回。

在上⾯的OnButton2()函数中,我要等待⼀个线程退出,将fWaitAll设置为
FALSE,⽬的是⽆论是线程真的退出了,还是有消息到达了,该函数都能返回。

如果将该fWaitAll设置为TRUE,那么函数返回的唯⼀条件是线程退出了,即便
是有消息到来了,该函数也⼀样不会返回。

DWORD dwMilliseconds:等待的事件,单位是毫秒。

可以设置为INFINITE,⽆穷等待
DWORD dwWakeMask:等待的消息类型,通常可以设置为QS_ALLINPUT。

此宏表⽰的是可以等待任意类型的消息。

当然,也可以指定等待的消息类型。

#define QS_ALLINPUT (QS_INPUT | \
QS_POSTMESSAGE | \
QS_TIMER | \
QS_PAINT | \
QS_HOTKEY | \
QS_SENDMESSAGE)
返回值:DWORD dRet 通过函数返回值,可以得到⼀些有效信息。

函数返回值依fWaitAll设置的不同⽽有所不同。

下⾯是函数返回值的⼏种常见类型:
dRet = 0xFFFFFFFF :表⽰函数调⽤失败,可⽤GetLastError()得到具体的出错信息;
dRet =WAIT_OBJECT_0+nCount:表⽰有消息到达了;
如果fWaitAll设置为TRUE
dRet = WAIT_OBJECT_0,表⽰所有等待的核⼼对象都激发了,或是线程都退出了;如果fWaitAll设置为FALSE
dRet = WAIT_OBJECT_0 ~ WAIT_OBJECT_0+nCount-1:
表⽰等待的内核对象被激发了,index=dRet -WAIT_OBJECT_0,
表⽰hArray[]数组中索引为index的那个对象被激发了。

当函数由于消息到来⽽返回,则需要⽤户主动去消息队列中将消息抓取出来,
然后派发出去,这样该消息就会被处理了。

其具体的操作就是:
while (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
下⾯再看⼀个⽤这个函数等待两个线程退出的例⼦:
//关闭线程1和2
void CThreadTest::OnButton6()
{


DWORD dRet=-2;
HANDLE hArray[2];
hArray[0]=m_pThread1->m_hThread;
hArray[1]=m_pThread2->m_hThread;
MSG msg;
int nExitThreadCount=0; //标记已经有⼏个线程退出了
BOOL bWaitAll=FALSE;
int nWaitCount=2; //初始等待的线程数⽬
while (1)
{
dRet=MsgWaitForMultipleObjects(nWaitCount,hArray,bWaitAll,INFINITE,QS_ALLINPUT);
if (dRet == WAIT_OBJECT_0+ nWaitCount)
{
TRACE("收到消息,函数返回值为%d \n",dRet);
while (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else if (dRet >= WAIT_OBJECT_0 && dRet < WAIT_OBJECT_0+ nWaitCount)
{
nExitThreadCount++;
if (nExitThreadCount == 1)
{
TRACE("⼀个线程退出了\n");
int nIndex=dRet-WAIT_OBJECT_0;
hArray[nIndex]=hArray[nWaitCount-1];
hArray[nWaitCount-1]=NULL;
nWaitCount--;
}
else
{
TRACE("两个线程都退出了\n");
break;
}
}
else
{
DWORD dErrCode=GetLastError();

break;
}
}
}
在上⾯这个例⼦中,我将bWaitAll设置为FALSE,
⽬的是当我要等待的两个线程中由⼀个退出了,或是有消息到来了,
此函数都可以退出。

如果我将此参数设置为TRUE,那么,
当且仅当我要等待的两个线程均退出了,这个函数才会返回,
这种使⽤⽅法有是程序陷⼊死锁的危险,故应避免。

⽆论是等待⼀个还是多个线程,只需将此参数设置为FALSE即可,
然后通过函数返回值判断究竟是那个返回了,
还是消息到达了即可。

这⼀要点前⾯已有陈述,此处再强调⼀遍。

通过函数返回值可以得知究竟哪个线程退出了,
当要等待的两个线程中的⼀个已经退出后,
则应该从新设置等待函数的参数,对等待的句柄数组进⾏整理。

{
int nIndex=dRet-WAIT_OBJECT_0;
hArray[nIndex]=hArray[nWaitCount-1];
hArray[nWaitCount-1]=NULL;
nWaitCount--;
}
这组语句就是⽤来从新设置参数的,其过程就是将等待的总数⽬减⼀,
并将刚退出的线程的句柄设置为NULL,移到数组的最末位置。

上⾯介绍了线程函数的设计以及在主线程中等待⼯作线程退出的⽅法,
着重介绍了MsgWaitForMultipleObjects函数的使⽤要点,
希望对⼤家有所帮助,也希望⼤家能提宝贵意见,补我之不⾜,愿与⼤家共同进步。

I have a Delphi 6 application that has a thread dedicated to communicating with a foreign application
that uses SendMessage() and WM_COPYDATA messages to interface with external programs.
Therefore, I create a hidden window with AllocateHWND() to service that need
since a thread message queue won't work due to the SendMessage() function only accepting window handles, not thread IDs. What I'm not sure about is what to put in the thread Execute() method.
I assume that if I use a GetMessage() loop or a create a loop with a WaitFor*() function
call in it that the thread will block and therefore the thread's WndProc() will never process the
SendMessage() messages from the foreign program right?
If so, what is the correct code to put in an Execute() loop that will not consume CPU cycles unnecessarily
but will exit once a WM_QUIT message is received?
I can always do a loop with a Sleep() if necessary but I'm wondering if there is a better way.
SendMessage() still requires the receiving thread to perform message retrieval (ie a message loop)
if the HWND belongs to another process.
AllocateHWnd() (more specifically, MakeObjectInstance()) is not thread-safe,
so you have to be careful with it. Better to use CreatWindow/Ex() directly instead.
In any case, an HWND is tied to the thread context that creates it,
so you have to create and destroy the HWND inside your Execute() method,
not in the thread's constructor/destructor.
Also, even though SendMessage() is being used to send the messages to you,
they are coming from another process, so they will not be processed by your HWND until its owning thread performs message retrieval operations,
so the thread needs its own a message loop.
Your Execute() method should look something like this:
procedure TMyThread.Execute;
var
Message: TMsg;
begin
FWnd := AllocateHWnd(WndProc);
try
while not Terminated do
begin
if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0then
begin
while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
begin
TranslateMessage(Message);
DispatchMessage(Message);
end;
end;
end;
finally
DeallocateHWnd(FWnd);
end;
end;
procedure TMyThread.WndProc(var Message: TMessage);
begin
if Message.Msg = WM_COPYDATA then
begin
...
Message.Result := ...;
end else
Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam, Message.LParam); end;
Isn't WaitMessage more natural here?
WaitMessage() does not return until a new message arrives, blocking the calling thread. MsgWaitForMultipleObjects() has a timeout, so the thread can wake up to do other things while the message queue is idle, like checking the Terminated property.
You can't do that WaitMessage() unless you post a message yourself.
But I think that posting message is better.
Don't want to have to wait for the timeout.
And if you do it that way then you can be properly idle.
Your code will wake up every second no matter what.。

相关文档
最新文档