来源:http://www.cnblogs.com/shangdawei/p/4015772.html
http://blog.csdn.net/byxdaz/article/details/5638680
用户模式的线程同步机制效率高,如果需要考虑线程同步问题,应该首先考虑用户模式的线程同步方法。
但是,用户模式的线程同步有限制,对于多个进程之间的线程同步,用户模式的线程同步方法无能为力。
这时,只能考虑使用内核模式。用户模式与内核模式线程同步机制比较
用户模式 内核模式 优点 线程同步机制速度快 支持多个进程之间的线程同步, 防止死锁 缺点 容易陷入死锁状态 线程同步机制速度慢 多个进程之间的线程同步会出现问题。 线程必须从用户模式转为内核模式。 (比如竞争资源、死锁) 这个转换需要很大的代价: 往返一次需要占用x 8 6平台上的大约1 0 0 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 ) { //超时 }
主线程使用WaitForSingleObject和MsgWaitForMultipleObjects等待线程返回值
多线程中,一般主线程创建线程(CreateThread)后,由工作线程函数完成具体内容,
工作线程在返回时通过发消息PostMessage告诉主线程结果,主线程做相当处理。
(也可以使用一个全局的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吧。。。
线程函数的设计以及MsgWaitForMultipleObjects函数的使用要点
使用多线程技术可以显著地提高程序性能,本文就讲讲在程序中如何使用工作线程,以及工作线程与主线程通讯的问题。
一 创建线程
使用MFC提供的全局函数AfxBeginThread()即可创建一个工作线程。
线程函数的标准形式为UINT MyFunProc(LPVOID );
此函数既可以是全局函数,也可以是类的静态成员函数。
之所以必须是静态成员函数,是由于类的非静态成员函数,
编译器在编译时会自动加上一个this指针参数,
如果将函数设置为静态的成员函数,则可以消除this指针参数。
如果想在线程函数中任意调用类的成员变量(此处指的是数据成员,而不是控件关联的成员变量),
则可以将类的指针作为参数传递给线程函数,然后经由该指针,就可以调用类的成员变量了。
//线程函数,类的静态成员函数 UINT CThreadTest::TH_SetProgress(LPVOID lpVoid) { CThreadTest *pTest=(CThreadTest *)lpVoid; pTest->SetProgress(); return 0; } //类的成员函数,此函数执行实际的线程函数操作,却可以自如的调用成员数据 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) { //继续分析 // } } } return 0; }
上面的线程函数用到了三个事件变量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 );//等待的消息类型
下面就详解一下该函数的参数使用方法:
DWORDnCount:要等待的内核对象的数目。如果等待两个线程退出,则nCount=2;
LPHANDLEpHandles:要等待的内核对象句柄数组指针。
如果只要等待一个线程退出,则直接设置该线程句柄的指针即可:
MsgWaitForMultipleObjects(1,&m_pThread->m_hThread,…)
如果要等待两个线程退出,则使用方法为:
HANDLE hArray[2]={ m_pThread1->m_hThread , m_pThread2->m_hThread };
MsgWaitForMultipleObjects(2,hArray,…)
BOOLfWaitAll: TRUE-表示只有要等待的线程全部退出后,此函数才返回,
FALSE-表示要等待的线程中任意一个退出了,或是有消息到达了,此函数均会返回。
在上面的OnButton2()函数中,我要等待一个线程退出,将fWaitAll设置为
FALSE,目的是无论是线程真的退出了,还是有消息到达了,该函数都能返回。
如果将该fWaitAll设置为TRUE,那么函数返回的唯一条件是线程退出了,即便
是有消息到来了,该函数也一样不会返回。
DWORDdwMilliseconds:等待的事件,单位是毫秒。 可以设置为INFINITE,无穷等待
DWORDdwWakeMask:等待的消息类型,通常可以设置为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函数的使用要点,
希望对大家有所帮助,也希望大家能提宝贵意见,补我之不足,愿与大家共同进步。