概述 :
非内核对象临界区非常适合于序列化对一个进程中的数据的访问,因为它们的速度很快。但我们或许想要使一些应用程序与计算机中发生的其它特殊事件或者其它进程中执行的操作取得同步。这时临界区无能为力。就需要使用内核对象来同步。
可以使用下列内核对象可用来同步线程:
1. 进程,Processes
2. 线程,Threads
3. 文件,Files
4. 控制台输入,Console input
5. 文件变化通知,File change notifications
6. 互斥量,Mutexes
7. 信号量,Semaphores
8. 事件(自动重设事件和手动重设事件),Events
9. 可等的计时器(只用于Window NT4或更高),Waitable timers
10. Jobs
每一个上面这些类型的对象都可以处于两种状态之一:有信号(signaled)和无信号(nonsignaled)。可用就是有信号状态,被占用就是无信号状态。比如进程和线程在终结时其内核对象变为有信号,而在它们处于创建和正在运行时,其内核对象是无信号的
内核对象同步应用:
1. 某线程获得某进程的内核对象句柄时,可以改变进程优先级、获得进程的退出码;使本线程与某进程的终结取得同步等等。
2. 当获得某线程的内核对象句柄时,可以:改变该线程运行状态、与该线程的终结取得同步等等。
3. 当获得文件句柄时,可以:本线程可与某一个异步文件的I/O操作获得同步等等。
4. 控制台输入对象可用来使线程在有输入进入时被唤醒以执行相关任务等等。
5. 其它内核对象―――文件改变通知、互斥量、信号量、事件、可等计时器等―――都只是为了同步对象而存在。相应的,也有WIN32函数来创建、打开、关闭这些对象,将线程与这些对象同步。
互斥量(Mutex)的独有特性:
Mutex,也就是互斥量,同步基元的意思。当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。 Mutex 只向一个线程授予对共享资源的独占访问权。 如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。
互斥量对象与所有其它内核对象的不同之处在于它是被线程所拥有的。其它所有同步对象要么有信号,要么无信号,仅此而已。而互斥量对象除了记录当前信号状态外,还要记住此时那个线程拥有它。如果一个线程在得到一个互斥量对象 (即将其置为无信号态)后就终结了,互斥量也就废弃了。在这种情况了,互斥量将永远保持无信号态,因为没有其它线程能够通过调用ReleaseMutex来释放它。
系统发现产生这种情况时,就自动将互斥量设回有信号状态。其它等待该信号量的线程就会被唤醒,但函数的返回值为WAIT_ABANDONED而不是正常的WAIT_OBJECT_0。这时,其它线程可以知道互斥量是不是被正常释放。
其它的,互斥量与CRITICAL_SECTION类似。拥有该互斥量的线程,每次调用WaitForSingleObject都会立即成功返回,但互斥量的使用计数将增加,同样的,也要多次调用ReleaseMutex以使引用计数变为零,方可供别的线程使用。
几点疑问:
问:其它内核对象在线程异常终止没有释放所有权时,系统回重置其状态吗?如果重置,将没有任何标记,与正常释放无异,即不会拥有互斥量的这个返回WAIT_ABANDONED的特性?
【注意:线程拥有某个内核对象和线程拥有某个内核对象的所有权,这二者是不同的。当说线程拥有某个内核对象时,要强调的是当该线程终止时,若线程正好拥有该内核对象的访问权,内核对象也将被废弃,因为不能重置其信号状态;而线程拥有某一个内核对象的所用权,指的是线程可以调用某些函数,访问该内核对象或对该内核对象执行某些操作】
1:互斥量(Mutex)
以互斥内核对象来保持线程同步可能用到的函数主要有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为待释放的互斥对象句柄。至于WaitForSingleObject()和 WaitForMultipleObjects()等待函数在互斥对象保持线程同步中所起的作用与在其他内核对象中的作用是基本一致的,也是等待互斥内核对象的通知。
但是这里需要特别指出的是:在互斥对象通知引起调用等待函数返回时,等待函数的返回值不再是通常的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()函数)。以此来表明线程正在等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止。除此之外,使用互斥对象的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时失去可调度性,而使用互斥的方法却可以在等待的同时仍具有可调度性,这也正是互斥对象所能完成的非常规操作之一。
在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存块时都对其拥有可靠的独占访问权。
2:信号量(Semaphores)
信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用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,如果需要也可以设置其他的值。WaitForSingleObject()和WaitForMultipleObjects()主要用在试图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。
信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为没一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。
事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。信号量包含的几个操作原语:
CreateEvent() 创建一个信号量
OpenEvent() 打开一个事件
SetEvent() 回置事件
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时)。
总结::
1:线程等待操作
线程主要使用两个函数将本身设为睡眠来等待内核对象变为有信号:即这两个函数都是阻塞函数。
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles, // HANDLE数组
BOOL bWaitAll,
DWORD dwMilliseconds
);
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不会在没有获得所有被等对象所有权的情形下改变某一可以被改变状态的内核对象的信号状态,任何以同样方式等待的线程都不会被唤醒,但以其它方式等待的线程将被唤醒。
2:比较分析
(1) 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
(2) 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。
(3) 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。