4一个经典的多线程同步问题.docx
- 文档编号:10505879
- 上传时间:2023-02-17
- 格式:DOCX
- 页数:33
- 大小:174.31KB
4一个经典的多线程同步问题.docx
《4一个经典的多线程同步问题.docx》由会员分享,可在线阅读,更多相关《4一个经典的多线程同步问题.docx(33页珍藏版)》请在冰豆网上搜索。
4一个经典的多线程同步问题
一个经典的多线程同步问题
程序描述:
主线程启动10个子线程并将表示子线程序号的变量地址作为参数传递给子线程。
子线程接收参数->sleep(50)->全局变量++->sleep(0)->输出参数和全局变量。
要求:
1.子线程输出的线程序号不能重复。
2.全局变量的输出必须递增。
下面画了个简单的示意图:
分析下这个问题的考察点,主要考察点有二个:
1.主线程创建子线程并传入一个指向变量地址的指针作参数,由于线程启动须要花费一定的时间,所以在子线程根据这个指针访问并保存数据前,主线程应等待子线程保存完毕后才能改动该参数并启动下一个线程。
这涉及到主线程与子线程之间的同步。
2.子线程之间会互斥的改动和输出全局变量。
要求全局变量的输出必须递增。
这涉及到各子线程间的互斥。
下面列出这个程序的基本框架,可以在此代码基础上进行修改和验证。
//经典线程同步互斥问题
#include
#include
#include
longg_nNum;//全局资源
unsignedint__stdcallFun(void*pPM);//线程函数
constintTHREAD_NUM=10;//子线程个数
intmain()
{
g_nNum=0;
HANDLEhandle[THREAD_NUM];
inti=0;
while(i { handle[i]=(HANDLE)_beginthreadex(NULL,0,Fun,&i,0,NULL); i++;//等子线程接收到参数时主线程可能改变了这个i的值 } //保证子线程已全部运行结束 WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE); return0; } unsignedint__stdcallFun(void*pPM) { //由于创建线程是要一定的开销的,所以新线程并不能第一时间执行到这来 intnThreadNum=*(int*)pPM;//子线程获取参数 Sleep(50);//someworkshouldtodo g_nNum++;//处理全局资源 Sleep(0);//someworkshouldtodo printf("线程编号为%d全局资源值为%d\n",nThreadNum,g_nNum); return0; } 运行结果: 可以看出,运行结果完全是混乱和不可预知的。 运用Windows平台下各种手段包括关键段,事件,互斥量,信号量等等来解决这个问题。 关键段CRITICAL_SECTION 首先介绍下如何使用关键段,然后再深层次的分析下关键段的实现机制与原理。 关键段CRITICAL_SECTION一共就四个函数,使用很是方便。 下面是这四个函数的原型和使用说明。 函数功能: 初始化 函数原型: voidInitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 函数说明: 定义关键段变量后必须先初始化。 函数功能: 销毁 函数原型: voidDeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 函数说明: 用完之后记得销毁。 函数功能: 进入关键区域 函数原型: voidEnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 函数说明: 系统保证各线程互斥的进入关键区域。 函数功能: 离开关关键区域 函数原型: voidLeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 然后在经典多线程问题中设置二个关键区域。 一个是主线程在递增子线程序号时,另一个是各子线程互斥的访问输出全局资源时。 详见代码: #include #include #include longg_nNum; unsignedint__stdcallFun(void*pPM); constintTHREAD_NUM=10; //关键段变量声明 CRITICAL_SECTIONg_csThreadParameter,g_csThreadCode; intmain() { printf("经典线程同步--关键段\n"); //关键段初始化 InitializeCriticalSection(&g_csThreadParameter); InitializeCriticalSection(&g_csThreadCode); HANDLEhandle[THREAD_NUM]; g_nNum=0; inti=0; while(i { EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域 handle[i]=(HANDLE)_beginthreadex(NULL,0,Fun,&i,0,NULL); ++i; } WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE); DeleteCriticalSection(&g_csThreadCode); DeleteCriticalSection(&g_csThreadParameter); return0; } unsignedint__stdcallFun(void*pPM) { intnThreadNum=*(int*)pPM; LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域 Sleep(50);//someworkshouldtodo EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域 g_nNum++; Sleep(0);//someworkshouldtodo printf("线程编号为%d全局资源值为%d\n",nThreadNum,g_nNum); LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域 return0; } 运行结果: 可以看出来,各子线程已经可以互斥的访问与输出全局资源了,但主线程与子线程之间的同步还是有点问题。 这是为什么了? 要解开这个迷,最直接的方法就是先在程序中加上断点来查看程序的运行流程。 断点处置示意如下: 然后按F5进行调试,正常来说这两个断点应该是依次轮流执行,但实际调试时却发现不是如此,主线程可以多次通过第一个断点即 EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域 这一语句。 这说明主线程能多次进入这个关键区域! 找到主线程和子线程没能同步的原因后,下面就来分析下原因的原因吧 先找到关键段CRITICAL_SECTION的定义吧,它在WinBase.h中被定义成RTL_CRITICAL_SECTION。 而RTL_CRITICAL_SECTION在WinNT.h中声明,它其实是个结构体: typedefstruct_RTL_CRITICAL_SECTION{ PRTL_CRITICAL_SECTION_DEBUGDebugInfo; LONGLockCount; LONGRecursionCount; HANDLEOwningThread;//fromthethread'sClientId->UniqueThread HANDLELockSemaphore; DWORDSpinCount; }RTL_CRITICAL_SECTION,*PRTL_CRITICAL_SECTION; 各个参数的解释如下: 第一个参数: PRTL_CRITICAL_SECTION_DEBUGDebugInfo; 调试用的。 第二个参数: LONGLockCount; 初始化为-1,n表示有n个线程在等待。 第三个参数: LONGRecursionCount; 表示该关键段的拥有线程对此资源获得关键段次数,初为0。 第四个参数: HANDLEOwningThread; 即拥有该关键段的线程句柄,微软对其注释为——fromthethread'sClientId->UniqueThread 第五个参数: HANDLELockSemaphore; 实际上是一个自复位事件。 第六个参数: DWORDSpinCount; 旋转锁的设置,单CPU下忽略 由这个结构可以知道关键段会记录拥有该关键段的线程句柄即关键段是有“线程所有权”概念的。 事实上它会用第四个参数OwningThread来记录获准进入关键区域的线程句柄,如果这个线程再次进入,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。 其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。 因此可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。 回到这个经典线程同步问题上,主线程正是由于拥有“线程所有权”即房卡,所以它可以重复进入关键代码区域从而导致子线程在接收参数之前主线程就已经修改了这个参数。 所以关键段可以用于线程间的互斥,但不可以用于同步。 另外,由于将线程切换到等待状态的开销较大,因此为了提高关键段的性能,Microsoft将旋转锁合并到关键段中,这样EnterCriticalSection()会先用一个旋转锁不断循环,尝试一段时间才会将线程切换到等待状态。 下面是配合了旋转锁的关键段初始化函数 函数功能: 初始化关键段并设置旋转次数 函数原型: BOOLInitializeCriticalSectionAndSpinCount( LPCRITICAL_SECTIONlpCriticalSection, DWORDdwSpinCount); 函数说明: 旋转次数一般设置为4000。 函数功能: 修改关键段的旋转次数 函数原型: DWORDSetCriticalSectionSpinCount( LPCRITICAL_SECTIONlpCriticalSection, DWORDdwSpinCount); 《Windows核心编程》第五版的第八章推荐在使用关键段的时候同时使用旋转锁,这样有助于提高性能。 值得注意的是如果主机只有一个处理器,那么设置旋转锁是无效的。 无法进入关键区域的线程总会被系统将其切换到等待状态。 最后总结下关键段: 1.关键段共初始化化、销毁、进入和离开关键区域四个函数。 2.关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。 3.推荐关键段与旋转锁配合使用。 事件Event 使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权”特性所以关键段只能用于线程的互斥而不能用于同步。 本篇介绍用事件Event来尝试解决这个线程同步问题。 首先介绍下如何使用事件。 事件Event实际上是个内核对象,它的使用非常方便。 下面列出一些常用的函数。 第一个CreateEvent 函数功能: 创建事件 函数原型: HANDLECreateEvent( LPSECURITY_ATTRIBUTESlpEventAttributes, BOOLbManualReset, BOOLbInitialState, LPCTSTRlpName ); 函数说明: 第一个参数表示安全控制,一般直接传入NULL。 第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。 如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。 打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。 自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。 第三个参数表示事件的初始状态,传入TRUR表示已触发。 第四个参数表示事件的名称,传入NULL表示匿名事件。 第二个OpenEvent 函数功能: 根据名称获得一个事件句柄。 函数原型: HANDLEOpenEvent( DWORDdwDesiredAccess, BOOLbInheritHandle, LPCTSTRlpName //名称 ); 函数说明: 第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。 详细解释可以查看MSDN文档。 第二个参数表示事件句柄继承性,一般传入TRUE即可。 第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。 第三个SetEvent 函数功能: 触发事件 函数原型: BOOLSetEvent(HANDLEhEvent); 函数说明: 每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。 第四个ResetEvent 函数功能: 将事件设为末触发 函数原型: BOOLResetEvent(HANDLEhEvent); 最后一个事件的清理与销毁 由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。 在经典多线程问题中设置一个事件和一个关键段。 用事件处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。 详见代码: #include #include #include longg_nNum; unsignedint__stdcallFun(void*pPM); constintTHREAD_NUM=10; //事件与关键段 HANDLEg_hThreadEvent; CRITICAL_SECTIONg_csThreadCode; intmain() { printf("经典线程同步事件Event\n"); //初始化事件和关键段自动置位,初始无触发的匿名事件 g_hThreadEvent=CreateEvent(NULL,FALSE,FALSE,NULL); InitializeCriticalSection(&g_csThreadCode); HANDLEhandle[THREAD_NUM]; g_nNum=0; inti=0; while(i { handle[i]=(HANDLE)_beginthreadex(NULL,0,Fun,&i,0,NULL); ResetEvent(g_hThreadEvent); WaitForSingleObject(g_hThreadEvent,INFINITE);//等待事件被触发 i++; } WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE); //销毁事件和关键段 CloseHandle(g_hThreadEvent); DeleteCriticalSection(&g_csThreadCode); return0; } unsignedint__stdcallFun(void*pPM) { intnThreadNum=*(int*)pPM; //SetEvent(g_hThreadEvent);//触发事件 Sleep(50);//someworkshouldtodo EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(0);//someworkshouldtodo printf("线程编号为%d全局资源值为%d\n",nThreadNum,g_nNum); LeaveCriticalSection(&g_csThreadCode); SetEvent(g_hThreadEvent); return0; } 运行结果: 可以看出来,经典线线程同步问题已经圆满的解决了——线程编号的输出没有重复,说明主线程与子线程达到了同步。 全局资源的输出是递增的,说明各子线程已经互斥的访问和输出该全局资源。 现在我们知道了如何使用事件,但学习就应该要深入的学习,何况微软给事件还提供了PulseEvent()函数,所以接下来再继续深挖下事件Event,看看它还有什么秘密没。 先来看看这个函数的原形: 第五个PulseEvent 函数功能: 将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。 函数原型: BOOLPulseEvent(HANDLEhEvent); 函数说明: 这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种: 1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。 2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。 此后事件是末触发的。 该函数不稳定,因为无法预知在调用PulseEvent()时哪些线程正处于等待状态。 下面对这个触发一个事件脉冲PulseEvent()写一个例子,主线程启动7个子线程,其中有5个线程Sleep(10)后对一事件调用等待函数(称为快线程),另有2个线程Sleep(100)后也对该事件调用等待函数(称为慢线程)。 主线程启动所有子线程后再Sleep(50)保证有5个快线程都正处于等待状态中。 此时若主线程触发一个事件脉冲,那么对于手动置位事件,这5个线程都将顺利执行下去。 对于自动置位事件,这5个线程中会有中一个顺利执行下去。 而不论手动置位事件还是自动置位事件,那2个慢线程由于Sleep(100)所以会错过事件脉冲,因此慢线程都会进入等待状态而无法顺利执行下去。 代码如下: //使用PluseEvent()函数 #include #include #include #include HANDLEg_hThreadEvent; //快线程 unsignedint__stdcallFastThreadFun(void*pPM) { Sleep(10);//用这个来保证各线程调用等待函数的次序有一定的随机性 printf("%s启动\n",(PSTR)pPM); WaitForSingleObject(g_hThreadEvent,INFINITE); printf("%s等到事件被触发顺利结束\n",(PSTR)pPM); return0; } //慢线程 unsignedint__stdcallSlowThreadFun(void*pPM) { Sleep(100); printf("%s启动\n",(PSTR)pPM); WaitForSingleObject(g_hThreadEvent,INFINITE); printf("%s等到事件被触发顺利结束\n",(PSTR)pPM); return0; } intmain() { printf("使用PluseEvent()函数\n"); BOOLbManualReset=FALSE; //创建事件第二个参数手动置位TRUE,自动置位FALSE g_hThreadEvent=CreateEvent(NULL,bManualReset,FALSE,NULL); if(bManualReset==TRUE) printf("当前使用手动置位事件\n"); else printf("当前使用自动置位事件\n"); charszFastThreadName[5][30]={"快线程","快线程","快线程","快线程","快线程"}; charszSlowThreadName[2][30]={"慢线程","慢线程"}; inti; for(i=0;i<5;i++) _beginthreadex(NULL,0,FastThreadFun,szFastThreadName[i],0,NULL); for(i=0;i<2;i++) _beginthreadex(NULL,0,SlowThreadFun,szSlowThreadName[i],0,NULL); Sleep(50);//保证快线程已经全部启动 printf("现在主线程触发一个事件脉冲-PulseEvent()\n"); PulseEvent(g_hThreadEvent);//调用PulseEvent()就相当于同时调用下面二句 //SetEvent(g_hThreadEvent); //ResetEvent(g_hThreadEvent); Sleep(3000); printf("时间到,主线程结束运行\n"); CloseHandle(g_hThreadEvent); return0; } 运行结果: 手动置位代码 intmain() { printf("使用PluseEvent()函数\n"); BOOLbManualReset=TRUE; //创建事件第二个参数手动置位TRUE,自动置位FALSE g_hThreadEvent=CreateEvent(NULL,bManualReset,FALSE,
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 一个 经典 多线程 同步 问题