您的当前位置:首页正文

线程同步

2022-10-23 来源:九壹网
同步---CriticalSection,Mutex,Event,Semaphores

关于线程的同步对象可分为内核对象与非内核对象,最大区别在于内核对象能跨越进程,而非内核对象不能跨越进程,只能同步单个进程中的线程。

内核对象:(非内核对象: CriticalSection)

1. 进程,Processe 2. 线程,Threads 3. 文件,Files 4. 控制台输入,Console input

5. 文件变化通知,File change notifications 6. 互斥量,Mutexes 7. 信号量,Semaphores 8. 事件Events 9. 可等的计时器Waitable timers 10.Jobs

每一个上面这些类型的对象都可以处于两种状态之一:有信号(signaled)和无信号(nonsignaled)。可用就是有信号状态,被占用就是无信号状态。比如进程和线程在终结时其内核对象变为有信号,而在它们处于创建和正在运行时,其内核对象是无信号的。

内核对象同步应用:

1.某线程获得某进程的内核对象句柄时,可以改变进程优先级、获得进程的退出码;使本线程与某进程的终结取得同步等。

2.当获得某线程的内核对象句柄时,可以改变该线程运行状态、与该线程的终结取得同步等。 3.当获得文件句柄时,本线程可与某一个异步文件的I/O操作获得同步等。 4.控制台输入对象可用来使线程在有输入进入时被唤醒以执行相关任务等。

5.其它内核对象―――文件改变通知、互斥量、信号量、事件、可等计时器等―――都只是为了同步对象而存在

下面详细介绍一下常用的同步对象: CritiaclSection:

临界区是保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

在所有同步对象中,临界区是最容易使用的,但它只能用于同步单个进程中的线程,并且不是内核对象,它不由操作系统的低级部件管理,而且不能使用句柄来操纵,由于不是内核对象,使得它作为一种轻量级的同步机制,同步速度比较快。 使用步骤:

1.在进程中创建一个临界区,即在进程中分配一个CRITICAL_SECTION数据结构,该临界区结构的分配必须是全局的,这样该进程的不同线程就能访问它。关于CRITICAL_SECTION结构体的深入分析,可以参见文章<>

2.在使用临界区同步线程之前,必须调用InitializeCriticalSection来初始化临界区。在释放资源之前,只需要初始化一次。

3.VOID EnterCriticalSection:阻塞函数。调用线程不能获取指定临界区的所有权时,该线程将睡眠,且在被唤醒之前,系统不会给它分配CPU。或者使用TryEnterCriticalSection方法尝试进入临界区,如果进入成功,则调用者线程获得临界区的使用权,否则返回失败。

4.执行临界区内的任务。

5.BOOL LeaveCriticalSection:非阻塞函数。将当前线程对指定临界区的引用计数减1;在使用计数变为零时,另一等待此临界区的一个线程将被唤醒。

6.当不需要再使用该临界区时,使用DeleteCriticalSection来释放临界区需要的资源。此函数执行后,再也不能使用EnterCriticalSection和LeaveCriticalSection,除非再次使用InitializeCriticalSection初始化了该临界区。

注意事项:

1.临界区一次只允许一个线程访问,每个线程必须在试图操作临界区域数据之前调用该临界区域标志(即一个CRITICAL_SECTION全局变量)EnterCriticalSection后,其它想要获得访问权的线程都会置于睡眠状态,且在被唤醒以前,系统将停止为它们分配CPU时间片。换言之,临界区可以且仅可被一个线程拥有,当然,没有任何线程调用EnterCriticalSection或TryEnterCriticalSection时,临界区不属于任何 一个线程。

2.当拥有临界区所有权的线程调用LeaveCriticalSection放弃所有权时,系统只唤醒等待队列中的一个线程,给它所有权,其它线程则继续等待。

3.注意,拥有该临界区的线程,每一次针对此临界区的EnterCriticalSection调用都会成功(这里指的是重复调用也会立即返回,也就是支持嵌套调用),且会使得临界区标志(即一个CRITICAL_SECTION全局变量)的引用计数增加1。在另一个线程能够拥有该临界区之前,拥有它的线程必须调用

LeaveCriticalSection足够多次,在引用计数降为零后,另一线程才有可能拥有该临界区。换言之,在一个正常使用临界区的线程中,calSection和LeaveCriticalSection应该成对使用。

4.TryEnterCriticalSection

BOOL TryEnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 从函数声明便可看出,EnterCriticalSection函数的返回值为VOID,而这里为BOOL。可见对于

TryEnterCriticalSection的调用,需要我们判断其返回值。在调用TryEnterCriticalSection时,如果指定的临界区没有被任何线程(或还没有被任何调用线程)拥有,该函数将临界区的访问权给予调用的线程,并返回TRUE;不过,如果临界区已经被另一个线程拥有,它立刻返回FALSE值。TryEnterCriticalSection和EnterCriticalSection之间的最大区别在于TryEnterCriticalSection从来不挂起线程。

Mutex:(互斥对象包含一个使用数量,一个线程ID和一个引用计数器)

当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex只向一个线程授予对共享资源的独占访问权。 如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。

互斥量对象与所有其它内核对象的不同之处在于它是被线程所拥有的。其它所有同步对象要么有信号,要么无信号,仅此而已。而互斥量对象除了记录当前信号状态外,还要记住此时那个线程拥有它。如

果一个线程在得到一个互斥量对象 (即将其置为无信号态)后就终结了,互斥量也就废弃了。在这种情况下,互斥量将永远保持无信号态,因为没有其它线程能够通过调用ReleaseMutex来释放它。系统发现产生这种情况时,就自动将互斥量设回有信号状态。(将线程ID置为零,引用计数置零)其它等待该信号量的线程就会被唤醒,但函数的返回值为WAIT_ABANDONED而不是正常的WAIT_OBJECT_0。这时,其它线程可以通过等待的返回值知道互斥量是不是被正常释放。

互斥量与CRITICAL_SECTION类似。拥有该互斥量的线程,每次调用WaitForSingleObject都会立即成功返回,但互斥量的使用计数将增加,同样的,也要多次调用ReleaseMutex以使引用计数变为零,方可供别的线程使用。

问:其它内核对象在线程异常终止没有释放所有权时,系统回重置其状态吗?

答:重置,但没有任何标记,与正常释放无异,即不会拥有互斥量的这个返回WAIT_ABANDONED的特性。

注意:线程拥有某个内核对象和线程拥有某个内核对象的所有权,这二者是不同的。当说线程拥有某个内核对象时,强调的是当该线程终止时,若线程正好拥有该内核对象的访问权,内核对象也将被废弃,因为不能重置其信号状态;而线程拥有某一个内核对象的所用权,指的是线程可以调用某些函数,访问该内核对象或对该内核对象执行某些操作。

以互斥内核对象来保持线程同步可能用到的函数主要有CreateMutex()、OpenMutex()、ReleaseMutex()、 WaitForSingleObject()和WaitForMultipleObjects()等。在使用互斥对象前,首先要通过 CreateMutex()或OpenMutex()创建或打开一个互斥对象。CreateMutex()函数原型为:

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性指针 BOOL bInitialOwner, // 初始拥有者 LPCTSTR lpName // 互斥对象名 );

参数bInitialOwner主要用来控制互斥对象的初始状态。一般多将其设置为FALSE,以表明互斥对象在创建时并没有为任何线程所占有。如果在创建互斥对象时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex()函数得到此互斥对象的句柄。 OpenMutex()函数原型为: HANDLE OpenMutex(

DWORD dwDesiredAccess, // 访问标志 BOOL bInheritHandle, // 继承标志 LPCTSTR lpName // 互斥对象名 );

当目前对资源具有访问权的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex()函数来释放其拥有的互斥对象,其函数原型为:BOOL ReleaseMutex(HANDLE hMutex),其唯一的参数hMutex为待释放的互斥对象句柄。

但是这里需要特别指出的是:在互斥对象通知引起调用等待函数返回时,等待函数的返回值不再是通常的WAIT_OBJECT_0(对于 WaitForSingleObject()函数)或是在WAIT_OBJECT_0到

WAIT_OBJECT_0+nCount-1之间的一个值(对于 WaitForMultipleObjects()函数),而是将返回一个WAIT_ABANDONED_0(对于 WaitForSingleObject()函数)或是在WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1之间的一个值(对于WaitForMultipleObjects()函数)。以此来表明线程正在等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止。 、

Event:(分为自动重置事件和人工重置事件)

事件对象也可以通过通知操作的方式来保持线程的同步。主要函数有: CreateEvent(),OpenEvent(),SetEvent(),ResetEvent(),WaitForSingleObject()和WaitForMultipleObjects()等。 使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。可以通过OpenEvent()函数获取得到,其函数原型为: HANDLE OpenEvent(

DWORD dwDesiredAccess, // 访问标志 BOOL bInheritHandle, // 继承标志

LPCTSTR lpName // 指向事件对象名的指针 );

如果事件对象已创建,函数将返回指定事件的句柄。对于那些在创建事件时没有指定事件名的事件内核对象,可以通过使用内核对象的继承性或是调用DuplicateHandle()函数来调用CreateEvent()以获得对指定事件对象的访问权。在获取到访问权后所进行的同步操作与在同一个进程中所进行的线程同步操作是一样的。

如果需要在一个线程中等待多个事件,则用WaitForMultipleObjects()来等待。

WaitForMultipleObjects()与WaitForSingleObject()类似,同时监视位于句柄数组中的所有句柄。这些被监视对象的句柄享有平等的优先权,任何一个句柄都不可能比其他句柄具有更高的优先权。WaitForMultipleObjects()的函数原型为: DWORD WaitForMultipleObjects( DWORD nCount, // 等待句柄数

CONST HANDLE *lpHandles, // 句柄数组首地址 BOOL fWaitAll, // 等待标志

DWORD dwMilliseconds // 等待时间间隔 );

参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount 个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。 dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回 WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某个值,则说明所有指定对象的状态均为已通知状态(当fWaitAll为TRUE时)或是用返回值减去WAIT_OBJECT_0可得到发生通知的对象的索引(当fWaitAll为FALSE 时)。如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知,且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用返回值减去WAIT_OBJECT_0可得到表示一个等待正常结束的互斥对象的索引(当fWaitAll为FALSE时)。

Semaphores:(允许多个线程同时访问一个资源)

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用

CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发

出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。 信号量是通过计数来对线程访问资源进行控制的,而实际上信号量确实也被称作Dijkstra计数器。

信号量内核对象进行线程同步主要会用到CreateSemaphore()、OpenSemaphore()、 ReleaseSemaphore()、WaitForSingleObject()和WaitForMultipleObjects()等函数。

CreateSemaphore()用来创建一个信号量内核对象,其函数原型为: HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针 LONG lInitialCount, // 初始计数 LONG lMaximumCount, // 最大计数 LPCTSTR lpName // 对象名指针 );

参数lMaximumCount是一个有符号32位值,定义了允许的最大资源计数,最大取值不能超过4294967295。lpName参数可以为创建的信号量定义一个名字,由于其创建的是一个内核对象,因此在其他进程中可以通过该名字而得到此信号量。

OpenSemaphore()函数即可用来根据信号量名打开在其他进程中创建的信号量,函数原型如下: HANDLE OpenSemaphore(

DWORD dwDesiredAccess, // 访问标志 BOOL bInheritHandle, // 继承标志 LPCTSTR lpName // 信号量名 );

在线程离开对共享资源的处理时,必须通过ReleaseSemaphore()来增加当前可用资源计数。否则将会出现当前正在处理共享资源的实际线程数并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。 ReleaseSemaphore()的函数原型为: BOOL ReleaseSemaphore(

HANDLE hSemaphore, // 信号量句柄 LONG lReleaseCount, // 计数递增数量 LPLONG lpPreviousCount // 先前计数 );

该函数将lReleaseCount中的值添加给信号量的当前资源计数,一般将lReleaseCount设置为1,如果需要也可以设置其他的值。

信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为没一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。

总结:

互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。

互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。

WaitForSingleObject,在一个指定时间(dwMilliseconds)内等待某一个内核对象变为有信号,在此时间内,若等待的内核对象一直是无信号的,则调用线程将睡眠,否则继续执行。超过此时间后,线程继续运行。函数返回值可能为:WAIT_OBJECT_0、WAIT_TIMEOUT、WAIT_ABANDONED(仅当内核对象为互斥量时)、WAIT_FAILED。

WaitForMultipleObjects与WaitForSingleObject类似,只是它要么等待指定列表(由lpHandles指定)中若干个对象(由nCount决定)都变为有信号,要么等待一个列表(由lpHandles指定)中的某一个对象变为有信号(由bWaitAll决定)。

WaitForSingleObject和WaitForMultipleObjects函数对特定的内核对象有重要的副作用,即它们根据不同的内核对象,会决定是否改变内核对象的信号状态,并执行这种改变;这些副作用,决定了是让等待该内核对象的进程或线程中的某一个被唤醒还是全都被唤醒。

(1) 对进程和线程内核对象,这两个函数不产生副作用。

在进程或线程内核对象变为有信号后,它们将保持有信号,这两个函数不会试图改变内核对象的信号状态。这样,所有等待这些内核对象的线程都会被唤醒。

(2) 对于互斥量、自动重置事件和自动重置可等的计时器对象,这两个函数将把它们的状态改为无信号。

换言之一旦这些对象变为有信号并且有一个线程被唤醒,则对象重被置为无信号状态。于是,只有一个正在等待的线程醒来,其它等待的线程将继续睡眠。

(3) 对于WaitForMultipleObjects函数还有非常重要的一个特性:当调用它时传递的bWaitAll为TRUE时,在所有被等待的对象都变为有信号之前,被等待的任何可以被改变状态的内核对象都不被重置为无信号状态。换言之,在传入参数bWaitAll为TRUE,WaitForMultipleObjects除非能取得所有指定对象(由lpHandles指定)的所有权,它不会取得单个对象的所有权(不能取得所有权,自然也不会改变此对象的信号状态)。这是为了防止死锁。换言之,在bWaitAll为TRUE时,WaitForMultipleObjects不会在没有获得所有被等对象所有权的情形下改变某一可以被改变状态的内核对象的信号状态,任何以同样方式等待的线程都不会被唤醒,但以其它方式等待的线程将被唤醒。

因篇幅问题不能全部显示,请点此查看更多更全内容