




已阅读5页,还剩13页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
【译】剖析MFC多线程程序的同步机制 原文链接:Synchronization in Multithreaded Applications with MFC 简介本文探讨基本的同步概念,并实际动手帮助新手掌握多线程编程。本文的重点在各种同步技巧。基本概念在线程执行过程中,或多或少都需要彼此交互,这种交互行为有多种形式和类型。例如,一个线程在执行完它被赋予的任务后,通知另一个线程任务已经完成。然后第二个线程做开始剩下的工作。下述对象是用来支持同步的:1)信号量2)互斥锁3)关键区域4)事件每个对象都有不同的目的和用途,但基本目的都是支持同步。当然还有其他可以用来同步的对象,比如进程和线程对象。后两者的使用由程序员决定,比如说判断一个给定进程或线程是否执行完毕为了使用进程和线程对象来进行同步,我们一般使用Wait*函数,在使用这些函数时,你应当知道一个概念,任何被作为同步对象的内核对象(关键区域除外)都处于两种状态之一:通知状态和未通知状态。例如,进程和线程对象,当他们开始执行时处于未通知状态,而当他们执行完毕时处于通知状态,为了判断一个给定进程或线程是否已经结束,我们必须判断表示其的对象是否处于通知状态,而要达到这样的目的,我们需要使用Wait*函数。Wait*函数 下面是最简单的Wait*函数:DWORDWaitForSingleObject(HANDLEhHandle,DWORDdwMilliseconds);参数hHandle表示待检查其状态(通知或者未通知)的对象,dwMilliseconds表示调用线程在被检查对象进入其通知状态前应该等待的时间。若对象处于通知状态或指定时间过去了,这个函数返回控制权给调用线程。若dwMilliseconds设置为INIFINITE(值为-1),则调用线程会一直等待直到对象状态变为通知,这有可能使得调用线程永远等待下去,导致“饿死”。 例如,检查指定线程是否正在执行, dwMilliseconds设置为0,是为了让调用线程马上返回。DWORDdw=WaitForSingleObject(hProcess,0);switch(dw)caseWAIT_OBJECT_0:/theprocesshasexitedbreak;caseWAIT_TIMEOUT:/theprocessisstillexecutingbreak;caseWAIT_FAILED:/failurebreak;下一个Wait类函数类似上面的,但它带的是一系列句柄,并且等待其中之一或全部进入已通知状态。DWORDWaitForMultipleObjects(DWORDnCount,CONSTHANDLE*lpHandles,BOOLfWaitAll,DWORDdwMilliseconds); 参数nCount表示待检查的句柄个数,lpHandles指向句柄数组,若fWaitAll为TRUE,则等待所有的对象进入已通知状态,若为FALSE,则当任何一个对象进入已通知状态时,函数返回。dwMilliseconds意义同上。 例如,下面代码判断哪个进程会先结束:HANDLEh3;h0=hThread1;h1=hThread2;h2=hThread3;DWORDdw=WaitForMultipleObjects(3,h,FALSE,5000);/任何一个进入已通知就返回switch(dw)caseWAIT_FAILED:/failurebreak;caseWAIT_TIMEOUT:/noprocessesexitedduring5000msbreak;caseWAIT_OBJECT_0+0:/aprocesswithh0descriptorhasexitedbreak;caseWAIT_OBJECT_0+1:/aprocesswithh1descriptorhasexitedbreak;caseWAIT_OBJECT_0+2:/aprocesswithh2descriptorhasexitedbreak; 句柄数组中索引号为index的对象进入已通知状态时,函数返回WAIT_OBJECT_0 + 索引号。若fWaitAll为TRUE,则当所有对象进入已通知状态时,函数返回WAIT_OBJECT_0。 一个线程若调用一个Wait*函数,则它从用户模式切换为内核模式。这带来的后果有好有坏。不好的是切换进入内核模式大概需要1000个时钟周期,这消耗不算小。好的是当进入内核模式后,就不需要使用处理器,而是进入休眠态,不参与处理器的调度了。 现在让我们进入MFC,并看看它能为我们做些什么。这里有两个类封装了对Wait*函数的调用: CSingleLock和CMultiLock。 同步对象等价的C+类EventsCEventCritical sectionsCCriticalSectionMutexesCMutexSemaphoresCSemaphore每个类都从一个类-CSyncObject继承下来,此类最有用的成员是重载的HANDLE运算符,它返回指定同步对象的内在句柄。所有这些类都定义在头文件中。事件 一般来说,事件用于这样的情形下:当指定的动作发生后,一个线程(或多个线程)才开始执行其任务。例如,一个线程可能等待必需的数据收集完后才开始将其保存到硬盘上。有两种事件:手动重置型和自动重置型。通过使用事件,我们可以轻松地通知另一个线程特定的动作已经发生了。对于手动重置型事件,线程使用它通知多个线程特定动作已经发生,而对于自动重置型事件,线程使用它只可以通知一个线程。在MFC中,CEvent类封装了事件对象(若在win32中,它是用一个HANDLE来表示的)。CEvent的构造函数运行我们选择创建手动重置型和自动重置型事件。默认的创建类型是自动重置型事件。为了通知正在等待的线程,我们可以调用CEvent:SetEvent方法,这个方法将会让事件进入已通知状态。若事件是手动重置型,则事件会保持已通知状态,直到对应的CEvent:ResetEvent被调用,这个方法将使得事件进入未通知状态。这个特性使得一个线程可以通过一个SetEvent调用去通知多个线程。若事件是自动重置型,则所有正在等待的线程中只有一个线程会接收到通知。当那个线程接收到通知后,事件会自动进入未通知状态。 下面两个例子将展示上述特性:/createanauto-reseteventCEventg_eventStart;UINTThreadProc1(LPVOIDpParam):WaitForSingleObject(g_eventStart,INFINITE);return0;UINTThreadProc2(LPVOIDpParam):WaitForSingleObject(g_eventStart,INFINITE);return0;在这个例子中,一个全局的CEvent对象被创建,当然它是自动重置型的。除此以外,有两个工作线程在等待这个事件对象以便开始其工作。只要第三个线程调用那个事件对象的SetEvent方法,则两个线程中之一(当然没人知道会是哪个)会接收到通知,然后事件会进入未通知状态,这就防止了第二个线程也得到事件的通知。 下面来看第二个例子:/createamanual-reseteventCEventg_eventStart(FALSE,TRUE);UINTThreadProc1(LPVOIDpParam):WaitForSingleObject(g_eventStart,INFINITE);return0;UINTThreadProc2(LPVOIDpParam):WaitForSingleObject(g_eventStart,INFINITE);return0;这段代码和上面的稍有不同,CEvent对象构造函数的参数不一样了,但意义上就大不同了,这是一个手动重置型事件对象。若第三个线程调用事件对象的SetEvent方法,则可以确保两个工作线程都会同时(几乎是同时)开始工作。这是因为手动重置型事件在进入已通知状态后,会保持此状态直到对应的ResetEvent被调用。 除此以外事件对象还有一个方法:CEvent:PulseEvent。这个方法首先使得事件对象进入已通知状态,然后使其退回到未通知状态。若事件是手动重置型,事件进入已通知状态会让所有正在等待的线程得到通知,然后事件进入未通知状态。若事件是自动重置型,事件进入已通知状态时只会让所有等待的线程之一得到通知。若没有线程在等待,则调用ResetEvent什么也不干。实例-工作者线程 本文所带的例子中,作者将展示如何创建工作者线程以及如何合理地销毁它们。作者定义了一个被所有线程使用的控制函数。当点击视图区域时,就创建一个线程。所有被创建的线程使用上述控制函数在视图客户区绘制一个运动的圆形。这里作者使用了一个手动重置型事件,它被用来通知所有工作线程其“死讯”。除此以外,我们将看到如何使得主线程等待直到所有工作者线程销毁掉。作者将线程函数定义为全局的:structTHREADINFOHWNDhWnd;/主视图区POINTpoint;/起始点;UINTThreadDraw(PVOIDpParam);externCEventg_eventEnd;UINTThreadDraw(PVOIDpParam)staticintsnCount=0;/线程计数器snCount+;/计数器递增TRACE(TEXT(-ThreadDraw%d:startedn),snCount);/取出传入的参数THREADINFO*pInfo=reinterpret_cast(pParam);CWnd*pWnd=CWnd:FromHandle(pInfo-hWnd);/主视图区CClientDCdc(pWnd);intx=pInfo-point.x;inty=pInfo-point.y;srand(UINT)time(NULL);CRectrectEllipse(x-25,y-25,x+25,y+25);CSizesizeOffset(1,1);/刷子颜色随机CBrushbrush(RGB(rand()%256,rand()%256,rand()%256);CBrush*pOld=dc.SelectObject(&brush);while(WAIT_TIMEOUT=:WaitForSingleObject(g_eventEnd,0)/只要主线程还未通知我自杀,继续工作!(注意时间设置为)CRectrectClient;pWnd-GetClientRect(rectClient);if(rectEllipse.leftrectClient.right)sizeOffset.cx*=-1;if(rectEllipse.toprectClient.bottom)sizeOffset.cy*=-1;dc.FillRect(rectEllipse,CBrush:FromHandle(HBRUSH)GetStockObject(WHITE_BRUSH);rectEllipse.OffsetRect(sizeOffset);dc.Ellipse(rectEllipse);Sleep(25);/休眠下,给其他绘制子线程运行的机会dc.SelectObject(pOld);deletepInfo;/删除参数,防止内存泄露TRACE(TEXT(-ThreadDraw%d:exiting.n),snCount-);/归还计数器return0; 注意作者传入的是一个安全句柄,而不是一个CWnd指针,并且在线程函数中通过传入的句柄创建一个临时的C+对象并使用。这样就避免了在多线程编程中多个对象引用单个C+对象的危险。CArraym_ThreadArray;/保存CWinThread对象指针/manual-reseteventCEventg_eventEnd(FALSE,TRUE);voidCWorkerThreadsView:OnLButtonDown(UINTnFlags,CPointpoint)THREADINFO*pInfo=newTHREADINFO;/线程参数pInfo-hWnd=GetSafeHwnd();/视图窗口pInfo-point=point;/当前点/将界面作为参数传入线程中,就可以在线程中自己更新主界面,而不用去通知主线程更新界面CWinThread*pThread=AfxBeginThread(ThreadDraw,(PVOID)pInfo,THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED);/创建线程,初始状态为挂起pThread-m_bAutoDelete=FALSE;/线程执行完毕后不自动销毁pThread-ResumeThread();/线程开始执行m_ThreadArray.Add(pThread);/保存创建的线程为了合理地销毁所有线程,首先使得事件进入已通知状态,这会通知工作线程“死期已至”,然后调用WaitForSingleObject让主线程等待所有的工作者线程完全销毁掉。注意每次迭代时调用WaitForSingleObject会导致从用户模式进入内核模式。例如,10此迭代会浪费掉大约10000次时钟周期。为了避免这个问题,我们可以使用WaitForMultipleObjects。这就是第二种方法。voidCWorkerThreadsView:OnDestroy()g_eventEnd.SetEvent();/*/*/第一种方式for(intj=0;jm_hThread,INFINITE);deletem_ThreadArrayj;*/第二种方式intnSize=m_ThreadArray.GetSize();HANDLE*p=newHANDLEnSize;for(intj=0;jm_hThread;:WaitForMultipleObjects(nSize,p,TRUE,INFINITE);for(intj=0;jnSize;j+)deletem_ThreadArrayj;deletep;TRACE(-CWorkerThreadsView:OnDestroy:finished!n);关键区域 和其他同步对象不同,除非有需要以外,关键区域工作在用户模式下。若一个线程想运行一个封装在关键区域中的代码,它首先做一个旋转封锁,然后等待特定的时间,它进入内核模式去等待关键区域。实际上,关键区域持有一个旋转计数器和一个信号量,前者用于用户模式的等待,后者用于内核模式的等待(休眠态)。在Win32API中,有一个CRITICAL_SECTION结构体表示关键区域对象。在MFC中,有一个类CCriticalSection。关键区域是这样一段代码,当它被一个线程执行时,必须确保不会被另一个线程中断。一个简单的例子是多个线程共用一个全局变量:intg_nVariable=0;UINTThread_First(LPVOIDpParam)if(g_nVariable100)return0;UINTThread_Second(LPVOIDpParam)g_nVariable+=50;return0; 这段代码不是线程安全的,因为没有线程对变量g_nVariable是独占使用的。为了解决这个问题,可以如下使用:CCriticalSectiong_cs;intg_nVariable=0;UINTThread_First(LPVOIDpParam)g_cs.Lock();if(g_nVariable100)g_cs.Unlock();return0;UINTThread_Second(LPVOIDpParam)g_cs.Lock();g_nVariable+=20;g_cs.Unlock();return0;这里使用了CCriticalSection类的两个方法,调用Lock函数通知系统下面代码的执行不能被中断,直到相同的线程调用Unlock方法。系统会首先检查被系统关键区域封锁的代码是否被另一个线程捕获。若是,则线程等待直到捕获线程释放掉关键区域。 若有多个共享资源需要保护,则最好为每个资源使用一个单独的关键区域。记得要配对使用UnLock和Lock。还有一点是需要防止“死锁”。classCSomeClassCCriticalSectionm_cs;intm_nData1;intm_nData2;public:voidSetData(intnData1,intnData2)m_cs.Lock();m_nData1=Function(nData1);m_nData2=Function(nData2);m_cs.Unlock();intGetResult()m_cs.Lock();intnResult=Function(m_nData1,m_nData2);m_cs.Unlock();returnnResult; 互斥锁和关键区域类似,互斥锁设计为对同步访问共享资源进行保护。互斥锁在内核中实现,因此需要进入内核模式操纵它们。互斥锁不仅能在不同线程之间,也可以在不同进程之间进程同步。要跨进程使用,则互斥锁应该是有名的。MFC中使用CMutex类来操纵互斥锁。可以如下方式使用:CSingleLocksingleLock(&m_Mutex);singleLock.Lock();/trytocapturethesharedresourceif(singleLock.IsLocked()/wedidit/usethesharedresource/Afterwedone,letotherthreadsusetheresourcesingleLock.Unlock();或者通过Win32函数:/trytocapturethesharedresource:WaitForSingleObject(m_Mutex,INFINITE);/usethesharedresource/Afterwedone,letotherthreadsusetheresource:ReleaseMutex(m_Mutex); 我们可以使用互斥锁来限制应用程序的运行实例为一个。可以将如下代码放置到InitInstance函数(或WinMain)中:HANDLEh=CreateMutex(NULL,FALSE,MutexUniqueName);if(GetLastError()=ERROR_ALREADY_EXISTS)/互斥锁已经存在AfxMessageBox(Aninstanceisalreadyrunning.);return(0);信号量 为了限制使用共享资源的线程数目,我们应该使用信号量。信号量是一个内核对象。它存储了一个计数器变量来跟踪使用共享资源的线程数目。例如,下面代码使用CSemaphore类创建了一个信号量对象,它确保在给定的时间间隔内(由构造函数第一个参数指定)最多只有5个线程能使用共享资源。还假定初始时没有线程获得资源:CSemaphoreg_Sem(5,5);一旦线程访问共享资源,信号量的计数器就减1.若变为0,则接下来对资源
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025江苏省退役军人事务厅直属优抚医院招聘12人考前自测高频考点模拟试题附答案详解
- 安全培训教学壁纸课件
- 2025年闭式塔项目合作计划书
- 2025湖南新宁县事业单位和县属国有企业人才引进降低开考比例岗位考前自测高频考点模拟试题及答案详解(易错题)
- 2025福建泉州发展集团有限公司(第一批)人才引进招聘25人模拟试卷及一套完整答案详解
- 客户信息采集及管理工具
- 小区农业设施共享管理协议
- 2025年安徽交控集团所属安徽交控石油有限公司招聘16人模拟试卷及答案详解(名师系列)
- 2025广东韶关市翁源县人民法院招聘劳动合同制书记员1人模拟试卷及答案详解(新)
- 医学研究成果安全保障承诺书(3篇)
- 《寻找消失的分数》期中考试分析班会课件
- 摩擦纳米发电机优化论文
- 2024年度浙江省选调生《行测》考试真题及答案
- 2025年上半年金华义乌市经信委招考易考易错模拟试题(共500题)试卷后附参考答案
- 兽医市场营销与管理试题及答案
- 数据中心锂离子电池消防安全白皮书
- 啤酒代理合作协议
- 甲亢护理诊断及措施
- 精雕雕刻机安全操作规程模版(2篇)
- 蓝耳病防控措施
- 护工护理员院感培训
评论
0/150
提交评论