Win32多线程编程-线程同步与通信_第1页
Win32多线程编程-线程同步与通信_第2页
Win32多线程编程-线程同步与通信_第3页
Win32多线程编程-线程同步与通信_第4页
Win32多线程编程-线程同步与通信_第5页
已阅读5页,还剩5页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、线程间数据通信系统从进程的地址空间中分配内存给线程栈使用。新线程与创建它的线程在相同的进程上下文中运行。因此,新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他所有线程的栈。这样一来,同一个进程中的多个线程可以很容易的相互通信。到目前为止,将数据从一个线程传到另一个线程的惟一方法是在创建线程时传递给新线程一个指针参数(LPVOIDlpParam)。参数lpParam为LPVOID指针类型,我们可在其中存储普通的数值(size为平台地址总线宽度),也可以存放指向某个数据结构(struct或class)的地址。在新线程函数中,解引用时需要强制类型转换回原类型,以进行正确的访

2、问。以下代码段演示了一个典型的多线程场景。/AtypicalmultithreadsceneDWORDWINAPIFirstThread(PVOIDlpParam)/Initializeastack-basedvariableintx=0;DWORDdwThreadID;/Createanewthread.HANDLEhThread=CreateThread(NULL,0,SecondThread,(LPVOID)&x,0,&dwThreadID);/Wedontreferencethenewthreadanymore,/socloseourhandletoit.CloseHandle(hTh

3、read);/Ourthreadisdone./BUG:ourstackwillbedestroyed,/butSecondThreadmighttrytoaccessit.return0;DWORDWINAPISecondThread(LPVOIDlpParam)/Dosomelengthyprocessinghere././AttempttoaccessthevariableonFirstThreadsstack./NOTE:Thismaycauseanaccessviolation-itdependsontiming!*(int*)lpParam)=5;/.return0;上述场景中,W

4、indows没有维持线程之间的“父子关系“,即父线程FirstThread已经终止运行,而子线程SecondThread仍在继续运行。以上父子关系只是为了一种解说上的方便,实际上FirstThread和SecondThread具有相同的优先级(默认是normal),因此它们“同时”执行。这样,FirstThread在开辟SecondThread后,不等SecondThread运行至代码*(int*)lpParam)=5;即可能退出。FirstThread栈上的自动变量x已销毁,而SecondThread试图去访问之,将导致AccessViolation。这是多线程编程中新手常犯的错误。解决以上

5、问题,大概有以下三种方案。让创建线程等待新线程退出后才退出。在FirstThread中代码CloseHandle(hThread);之前WaitForSingleObject(hThread,INFINITE);这样保证SecondThread中对FirstThread栈中自动变量x的访问有效期。将x声明为堆变量,即int*px=newint;,在SecondThread中对px进行访问完毕后调用deletepx;释放堆内存。由于堆内存对进程有效,因此,上述代码中FirstThread先退出,在SecondThread中对px的访问依然有效,直到进程的某处将该内存delete掉。这是在需要动态

6、创建线程参数(数据结构)时的一种解决方案,实际应用中经常用到。将x声明为静态变量staticintx=0;则将存储在静态存储区域。这里有全局和局部之分,若在FirstThread之前声明,则整个程序均可显式访问x;若在FirstThread之中声明,贝Ix只在FirstThread中可见。当然这里传址给SecondThread,SecondThread可按址访问。在方案(3)中,若在FirstThread之中将x声明为静态变量,将使函数SecondThread不可重入。换言之,不能创建两个使用相同线程函数的线程,因为这两个线程将共享同一个静态变量。这涉及到下文将要阐述的线程同步问题。二多线程同

7、步互斥问题1同步问题的导入多个线程共享数据时,同时读没有问题,但如果同时读和写,情况就不同了。在本次线程内,读取一个变量时,为提高存取速度,编译器优化时,有时会先把变量读取到一个寄存器中;以后取变量值时,就直接从寄存器中取值;当变量值在本线程中改变时,会同时把变量的新值拷贝到该寄存器中,以便保持一致;当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。/CountError#include#include#includeintg_nCount1=0;intg_nCount2=0;BOOLg_bContinue=TRUE;UINT_stdcal

8、lThreadFunc(LPVOID);intmain(intargc,char*argv)UINTuId;HANDLEh2;h0=(HANDLE):_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);h1=(HANDLE):_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);/等待1秒后通知两个计数线程结束,关闭句柄Sleep(1000);g_bContinue=FALSE;/等待两个线程都运行完:WaitForMultipleObjects(2,h,TRUE,INFINITE);:CloseHandle(h0)

9、;:CloseHandle(h1);printf(g_nCount1=%dn,g_nCount1);printf(g_nCount2=%dn,g_nCount2);return0;UINT_stdcallThreadFunc(LPVOID)while(g_bContinue)g_nCount1+;g_nCount2+;return0;以上代码中线程h0和h1(具有相同的线程函数ThreadFunc)同时增加全局变量g_nCount1和g_nCount2的计数。按道理来说最终在主线程中输出的它们的值应该是相同的,可是结果却并不尽如人意。上述测试中,g_nCount1和g_nCount2的值往往并

10、不相同。出现此种结果主要是因为同时访问g_nCount1和g_nCount2的两个线程具有相同的优先级。在执行过程中,如果第一个线程取走g_nCount1的值准备进行自加操作的时候,它的时间片恰好用完,系统切换到第二个线程去对g_nCount1进行自加操作;一个时间片过后,第一个线程再次被调度,此时它会将上次取出的值自加,并放入g_nCount1所在的内存里,这就会覆盖掉第二个线程对g_nCount1的自加操作。变量g_nCount2也存在相同的问题。由于这样的事情的发生次数是不可预知的,所以最终它们的值就不相同了。针对以上问题,可使用volatile修饰静态变量去优化,直接存取原始内存地址。

11、对于volatile变量,优化器在用到这个变量时每次都必须小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。这对于经常同硬件、中断、等等打交道的嵌入式系统程序而言,是一种很好的解决方案。但常规情况下,很少使用去优化的方式。上例中,g_nCount1和g_nCount2是全局变量,属于该进程内所有线程共有的资源。解决问题的关键在于在一个线程对某个对象进行操作的过程中,需要有某种机制阻止其他线程的操作,这将涉及到到同步、互斥等话题。2同步与互斥的概念同步与互斥往往像一对孪生兄弟,总是在同一语境中被提及,但往往语焉不详。下面厘清一下它们之间的暧昧。互斥:是指某一资源同时只允许一个访问者对其

12、进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。大街上的移动厕所往往只有一个茅坑,如果把厕所(茅坑)当做一种资源,则某一时刻,这种珍贵的资源只允许一人享用。这种对资源使用的独占性和排他性即互斥。上述上厕所场景中,互斥这一原则的约束下,内急者先来先上,维持了良好的公共秩序。从同步的制约性要素考虑,上厕所行为不存在“同步”问题。因为A上完厕所即可走人,接下来轮到B,B

13、进行与A几乎完全一样的独立操作,A和B也许素昧平生,它们之间不存在任何的制约关系。关于同步的典型案例是“生产者-消费者”模型。生产者占用缓冲区时,消费者不能占用,反之亦然,这个即互斥;消费者必须要等生产者生产之后,才能消费,这个即同步。在这里,同步与互斥形影相随,同步中暗含互斥。同时,可以看出,生产者和消费者的同步关系本质上是一种供需制约关系。3多线程同步策略多线程同步就要保证在一个线程占有公共资源的时候,其他线程不会再次占有这个资源所以,解决同步问题,就是保证整个存取过程的独占性。同步可以保证在一个时间内只有一个线程对某个共享资源有控制权,其本质是微观串行所体现出来的等待。之前谈到的那个计数

14、错误(CountError)问题,涉及到如何协调线程间的活动,以保证对资源的正确访问。Windows操作系统提供了多种同步手段,同步对象包括临界区(CriticalSection)、事件(Event)、信号量(Semaphore)、互斥量(Mutex)等。(1)临界区对象(CRITICAL_SECTION),也称关键代码段临界区对象依赖一个CRITICAL_SECTION数据结构记录一些信息,确保在同一时间只有一个线程访问该数据段中的数据。使用临界区实施同步,首先需要声明一个CRITICAL_SECTION结构。对CRITICAL_SECTION结构的操作包括Initialize、Enter/

15、Leave、Delete。编程的时候,要把临界区对象定义在想保护的数据段中,然后在任何线程使用此临界区对象之前,调用InitializeCriticalSection函数对它进行初始化。/TheInitializeCriticalSectionfunctioninitializesacriticalsectionobject.VOIDInitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection/criticalsection);之后,线程访问临界区中数据的时候,必须首先调用EnterCriticalSection函数,申请进入临界区

16、。在同一时间内,Windows只允许一个线程进入临界区。所以在申请的时候,如果有另一个线程在临界区的话,EnterCriticalSection函数会一直等待下去,直到其他线程离开临界区才返回。/TheEnterCriticalSectionfunctionwaitsforownershipofthespecifiedcriticalsectionobject.Thefunctionreturnswhenthecallingthreadisgrantedownership.VOIDEnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection/

17、criticalsection);当操作完成的时候,要调用LeaveCriticalSection函数将临界区交还给Windows系统,以便其他线程可以申请使用。否则,就是占着茅坑不拉屎憋死其他人的不道德行为。/TheLeaveCriticalSectionfunctionreleasesownershipofthespecifiedcriticalsectionobject.VOIDLeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection/criticalsection);当程序不再使用临界区对象的时候,必须使用DeleteCritic

18、alSection函数执行删除操作,释放资源。/TheDeleteCriticalSectionfunctionreleasesallresourcesusedbyanunownedcriticalsectionobject.VOIDDeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection/criticalsection);声明一个CRITICAL_SECTION对象,即政府修建了一座厕所。厕所这一资源起初是上锁的,Initialize可看做厕所开锁对外开放,使厕所可用。Enter可看做上厕所的排队过程,一旦茅坑空出,即可持票进入享用;

19、Leave可看做如厕完毕冲水走人。厕所被很多人Enter/Leave用了几年后,其使命结束,政府依据城市规划,将其拆掉一Delete收回。临界区对象能够很好地保护共享数据,但是它不能够用于进程之间资源的锁定。由于它不是内核对象,故临界区只能用于在同一进程内的线程同步。如果要在进程间维持线程的同步,可以使用事件内核对象。(2)事件内核对象(event)多线程程序设计大多会涉及线程间相互通信。主线程在创建工作线程的时候,可以通过参数给工作线程传递初始化数据,当工作线程开始运行后,还需要通过通信机制来控制工作线程。同样,工作线程有时候也需要将一些情况主动通知主线程。事件内核对象是一种比较好的通信方法

20、。事件对象(event)是一种抽象的对象,它也有未受信(nonsignaled)和受信(signaled)两种状态。编程人员也可以使用WaitForSingleObject/WaitForMultipleObjects函数等待其变成受信状态。不同于其他内核对象,系统提供的一些API可以使事件对象在这两种状态之间转化。可以把事件对象看成是一个设置在Windows内部的标志,它的状态设置和测试工作由Windows来完成。事件对象包含3个成员:nUsageCount(使用计数)、bManualReset(是否人工重置)和bSignaled(是否受信)。成员nUsageCount记录了当前的使用计数,

21、当使用计数为0的时候,Windows就会销毁此内核对象占用的资源;成员bManualReset指定在一个事件内核对象上等待的函数返回之后,Windows是否重置这个对象为未受信状态;成员bSignaled指定当前事件内核对象是否受信。下面要介绍的操作事件内核对象的函数会影响这些成员的值。要使用事件对象,首先用CreateEvent函数去创建它。/TheCreateEventfunctioncreatesoropensanamedorunnamedeventobject.HANDLECreateEvent(LPSECURITY_ATTRIBUTESlpEventAttributes,/SDBOO

22、LbManualReset,/resettypeBOOLbInitialState,/initialstateLPCTSTRlpName/objectname);参数一为事件对象的安全属性,一般填充NULL表示取默认值。参数二,选择事件对象的重置方式以决定类型。bManualReset=TRUE则表示人工重置(manual-reset);bManualReset=FALSE贝I表示自动重置(auto-reset)。当一个人工重置的事件对象受信以后,所有等待在这个事件上的线程都会变为可调度状态;当一个自动重置的事件对象受信以后,Windows仅允许一个等待在该事件上的线程变成可调度状态,然后就自

23、动重置此事件对象为未受信状态。通常使用自动重置的事件内核对象,即设置bManualReset=FALSE。参数三bInitialState对应着bSignaled成员的初态。若将它设为TRUE,则表示事件对象创建时的初始状态为受信;若将它设为FALSE,则初始状态为未受信。通常设置初始状态为未受信,即置bInitialState=FALSE。参数四指定事件对象的名称,以便跨进程按名访问。跨进程访问事件内核对象,可传入事件对象名调用OpenEvent获取该对象的句柄。/TheOpenEventfunctionopensanexistingnamedeventobject.HANDLEOpenEv

24、ent(DWORDdwDesiredAccess,/accessBOOLbInheritHandle,/inheritanceoptionLPCTSTRlpName/objectname);系统创建或打开一个事件内核对象后,会返回事件的句柄。当编程人员不使用此内核对象的时候,应该调用CloseHandle函数释放它占用的资源。事件对象被建立后,程序可以通过SetEvent和ResetEvent函数来设置它的状态。/TheSetEventfunctionsetsthespecifiedeventobjecttothesignaledstate.BOOLSetEvent();HANDLEhEven

25、t/handletoevent/TheResetEventfunctionsetsthespecifiedeventobjecttothenonsignaledstate.BOOLResetEvent(HANDLEhEvent/handletoevent);与SetEvent/ResetEvent相关的另一个函数是PulseEvent。顾名思义,所谓PulseEvent即瞬间Set/Reset,至于这个瞬间多长及其效用,这里不详解。可参考关于线程同步PulseEvent()/ThePulseEventfunctionsetsthespecifiedeventobjecttothesignale

26、dstateandthenresetsittothenonsignaledstateafterreleasingtheappropriatenumberofwaitingthreads.BOOLPulseEvent(HANDLEhEvent/handletoeventobject);事件内核对象的同步,代码上体现在对WaitForSingleObject/WaitForMultipleObjects函数的调用,等待事件对象的置信。我们可以将之想象为厕所门外的“有人/没人”信号灯,红灯有人(nonsignaled),绿灯无人(signaled)。事件对象是一个用于线程间通信被广泛使用的内核对象。

27、因为它是一个内核对象,所以也可以跨进程使用。依靠在线程间通信就可以使各线程的工作协调进行,达到同步的目的。(3)信号量内核对象(Semaphore)信号量内核对象对线程的同步方式与前面几种不同,它允许多个线程在同一时刻访问某一资源,但是需要限制同一时刻访问此资源的最大线程数目。首先使用CreateSemaphore函数创建信号量内核对象。/TheCreateSemaphorefunctioncreatesoropensanamedorunnamedsemaphoreobject.HANDLECreateSemaphore(LPSECURITY_ATTRIBUTESlpSemaphoreAttr

28、ibutes,/SDLONGlInitialCount,/initialcountLONGlMaximumCount,/maximumcountLPCTSTRlpName/objectname);CreateSemaphore函数创建信号量时,参数三指定允许的最大资源计数,参数二指定当前可用的初始资源计数。一般将lInitialCount设置与lMaximumCount相等。参数四即内核对象名称,以便跨进程执行OpenSemaphore按名访问。只要当前可用资源计数大于0,就可以发出信号量信号,在该信号量上的等待函数调用WaitForSingleObject返回。每增加一个线程对共享资源的访问

29、,当前可用资源计数就会减1。WaitForSingleObject返回后,调用线程在对共享资源的同步处理完毕后,应调用ReleaseSemaphore来增加当前可用资源计数。否则,将会出现当前正在处理共享资源的实际线程并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。/TheReleaseSemaphorefunctionincreasesthecountofthespecifiedsemaphoreobjectbyaspecifiedamount.BOOLReleaseSemaphore(HANDLEhSemaphore,LONGlReleaseCount,/h

30、andletosemaphore/countincrementamountLPLONGlpPreviousCount/previouscount);参数一为信号量内核对象句柄;参数二为计数递增值,一般设为1,当然也可以按需要设置大于1的值;参数三为之前的计数,往往填NULL,当然可指定导出到当地变量。信号量内核对象的同步,代码上体现在对WaitForSingleObject/WaitForMultipleObjects函数的调用,其同步条件为资源计数大于0,即有可用资源。某个线程处理完共享资源后,需要调用ReleaseSemaphore释放资源,增加可用资源计数。当然,同其他内核对象一样,最终

31、也得调用CloseHandle函数释放信号量内核对象占用的资源。不同于过于简陋的只有一个茅坑的移动厕所,豪华一点的公共厕所往往不止一个坑位。将坑位看做资源,则坑位的个数即信号量机制中的资源计数。这一排坑位,在同一时间内也只能满足部分人的需求,一个人一个坑,有人用完了释放坑位,排队的人才有机会进入。信号量的使用特点使其更适用于对Socket程序中线程的同步问题。一个典型的场景就是HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这是可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有

32、不大于设置的最大用户数目的线程能够进行访问,而其他访问企图被挂起。只有在有用户退出对此页面的访问后,其他用户的访问请求才有可能得到响应。迅雷的“原始地址线程数”即是设置客户端从某一原始地址下载资源的最大线程数。当然,资源所在的站点本身就会对某一客户连接数有限制,这里的“某一客户连接数”意即把文件拆开,一个线程下载一块的多线程协助下载。当然,多线程并不是越多越好,迅雷下载肯定使用的是线程池。迅雷将下载线程数限制为5,符合线程池的经验公式,即线程池规模=CPU数*2+1现在机器基本都是双核或多CPU的。当用户建立5个以上的下载任务时,迅雷最多同时执行5个下载任务,超出的任务将排队等待。一旦有下载任

33、务完成,另一个等待下载任务即启动。迅雷对于下载线程数的限制,即使用了信号量机制。(4)互斥内核对象(Mutex)互斥是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限。由于互斥对象只有一个,因此就决定了任何情况下,此共享资源都不会被多个线程所访问。当前占据资源的线程在任务处理完后应该将占据的互斥对象交出,以便其他线程在其上的等待调用WaitForSingleObject/WaitForMultipleObjects返回。基于互斥内核对象来保持线程同步用到的函数主要有CreateMutex、OpenMutex、Re

34、leaseMutex,其用法在代码布局上同信号量内核对象。/TheCreateMutexfunctioncreatesoropensanamedorunnamedmutexobject.HANDLECreateMutex(LPSECURITY_ATTRIBUTESlpMutexAttributes,/SDBOOLbInitialOwner,/initialownerLPCTSTRlpName/objectname);参数blnitialOwner主要用来控制互斥对象的初始状态,一般将其设为FALSE,以表明互斥对象在创建时并没有为任何线程所占有。最后一个参数即内核对象名称,以便跨进程执行Ope

35、nMutex按名访问。当目前对资源具有访问权限的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex函数来释放其拥有的互斥对象。/TheReleaseMutexfunctionreleasesownershipofthespecifiedmutexobject.BOOLReleaseMutex(HANDLEhMutex/handletomutex);基于互斥内核对象的同步在代码上体现在对WaitForSingleObject/WaitForMultipleObjects函数的调用,以等待互斥内核对象的通知,其同步条件为某一时刻只有一个线程拥有互斥对象。在互斥对象通知引起调用等待函

36、数返回时,等待函数的返回值不在是WAIT_OBJECT_0或WAIT_OBJECT_0,WAIT_OBJECT_0+nCount-1之间的某值,而是将返回WAIT_ABANDONED_0或是WAIT_ABANDONED_0,WAIT_ABANDONED_0+nCount-1之间的某值,以此来表明线程正在等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止除此之外,使用互斥内核对象的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时丧失可调度性,而使用互斥的方法可以在等待的同时仍具有调度性

37、,这也正是互斥对象所能完成的非常规操作之一。在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存快时,都对其拥有可靠的独占访问权。(5)其他(互锁函数和旋转锁)互锁函数互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同进程的线程也可以使用此机制。用于互锁的函数有InterlockedIncrement、InterlockedDecrement等。InterlockedIncrement函数递增(加1)指定的32位变量。这个函数可以阻止其他线程同时使用此变量,函数原型如下。/TheInterlockedIncrementfu

38、nctionincrements(increasesbyone)thevalueofthespecifiedvariableandcheckstheresultingvalue.Thefunctionpreventsmorethanonethreadfromusingthesamevariablesimultaneously.LONGInterlockedIncrement(LPLONGvolatilelpAddend/variabletoincrement);InterlockedDecrement函数同步递减(减1)指定的32位变量,原型如下。/TheInterlockedDecrementfunctiondecrements(decreasesbyone)thevalueofthespecifiedvariableandcheckstheresultingvalue.Thefunctionpreventsmorethanonethreadfromusingthesamevariablesimultaneously.LONGInt

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论