用内核对象进行线程同步
用户模式的线程同步机制速度快,如果需要考虑线程同步问题,应该首先考虑用户模式的线程同步方法。但是,用户模式的线程同步有限制,对于多个进程之间的线程同步,用户模式的线程同步方法无能为力。这时,只能考虑使用内核模式。
Windows提供了许多内核对象来实现线程的同步。对于线程同步而言,这些内核对象有两个非常重要的状态:“已触发”状态,“未触发状态。你可以触发一个内核对象,使之处于“已触发状态”,然后让其他等待在该内核对象上的线程继续执行。你可以使用Windows提供的API函数,等待函数来等待某一个或某些内核对象变为已触发状态。
1.等待函数
你可以使用WaitForSingleObject函数来等待一个内核对象变为已触发状态:
DWORD
该函数需要传递一个内核对象句柄,该句柄标识一个内核对象,如果该内核对象处于未触发状态,则该函数导致线程进入阻塞状态;如果该内核对象处于已触发状态,则该函数立即返回WAIT_OBJECT_0。第二个参数指明了需要等待的时间(毫秒),可以传递INFINITE指明要无限期等待下去。如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED
DWORD
switch
{
}
还可以使用WaitForMulitpleObjects函数来等待多个内核对象变为已触发状态:
DWORD
该函数的第一个参数指明等待的内核对象的个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一个值。phObjects参数是一个存放等待的内核对象句柄的数组。bWaitAll参数如果为TRUE,则只有当等待的所有内核对象为已触发状态时函数才返回,如果为FALSE,则只要一个内核对象为已触发状态,则该函数返回。第四个参数和WaitForSingleObject中的dwMilliseconds参数类似。
该函数失败,返回WAIT_FAILED;如果超时,返回WAIT_TIMEOUT;如果bWaitAll参数为TRUE,函数成功则返回WAIT_OBJECT_0,如果bWaitAll为FALSE,函数成功则返回值指明是哪个内核对象收到触发。
可以如下使用该函数:
HANDLE
//三个进程句柄
h[0]
h[1]
h[2]
DWORD
switch
{
}
2
我们在调用WaitForSingleObject或WaitForMultipleObjects有的时候可能
3
在所有内核对象中,事件内核对象是最基本的一个内核对象。在事件内核对象内部,有以下几个比较重要的数据:
1、有一个“引用计数”:指明被打开的次数;
2、一个“布尔值”:指明该事件内核对象是自动重置的还是人工重置的;
3、另一个“布尔值”:指明该事件内核对象是“已触发状态”还是“未触发状态”。
事件内核对象可以触发一个事件已经完成。有两种不同的类型:自动重置和人工重置。当人工重置的事件内核对象得到触发的时候,所有等待在事件内核对象上的线程都变成可调度线程。当一个自动重置的事件内核对象得到触发的时候,等待在该事件内核对象上的线程只有一个能变成可调度状态。
要使用事件内核对象,首先调用CreateEvent函数来创建一个事件内核对象:
HANDLE
bManualReset参数指定了该内核对象是人工重置(传递TRUE)的还是自动重置(传递FALSE)的。
bInitialState参数指定了该内核对象起始状态是已触发(传递TRUE)还是未触发状态(FALSE)。
pszName参数为要创建的事件内核对象起一个名字,如果传递NULL,则创建一个“匿名”的事件内核对象。如果不传递NULL,且系统中已经存在该名字的事件内核对象,则不创建新的事件内核对象而是打开这个已经存在的,返回它的句柄。
该函数如果成功,返回事件内核对象的句柄,这样就可以操纵它了。如果失败,返回NULL。
HANDLE
第一个参数指明的访问的限制,第二个参数表示该事件内核对象的句柄能够被子进程继承,第三个参数指明了该事件内核对象的名字。该函数成功返回事件内核对象的句柄,失败返回NULL。
当不需要使用这些句柄时,需要调用CloseHandle函数来递减内核对象的引用计数,使得该内核对象可以被及时清除。
当一个事件内核对象被创建之后,你可以直接控制它的状态。你可以触发它,使得它从未触发状态转变为已触发状态:
BOOL
也可以重新设置它,使它从已触发状态变为未触发状态:
BOOL
一个自动重置的事件内核对象,如果等待成功,由于“成功等待的副作用”机制会将该事件内核对象由已触发状态变为未触发状态,这个时候就没有必要调用ResetEvent函数了。
如果是一个人工重置的事件内核对象,等待成功之后,并不会被设置为未触发状态,而是要程序员调用ResetRvent函数来使之转变为未触发状态。
还有要注意的就是,一个“自动重置”的事件内核对象收到触发,转变为已触发状态的时候,最多只能唤醒“一个”等待在它上的线程。一个“人工重置”的事件内核对象收到触发,转变为已触发状态的时候,能够唤醒“所有”等待在它上的线程。
等待定时器(waitable
要创建一个等待定时器内核对象,可以调用函数CreateWaitableTimer。可以为该函数赋予不同的参数来指定一个定时器内核对象的属性。
HANDLE
第二个参数指明了该定时器内核对象是人工重置(TRUE)的还是自动重置(FALSE)的。该函数成功,返回句柄,失败则返回NULL。
当一个人工重置的定时器内核对象收到触发时,所有等待在该内核对象上的线程都可以被唤醒,进入就绪状态。一个自动重置的定时器内核对象收到触发时,只有一个等待在该内核对象上的线程可以被调度。
当然,也可以打开一个特定名字的定时器内核对象,呼叫OpenWaitableTimer函数:
HANDLE
等待定时器内核对象创建的时候的状态总是“未触发状态”。你可以呼叫SetWaitableTimer函数来设定等待定时器内核对象何时获得触发。
BOOL
该函数的第1个参数hTimer是一个等待定时器内核对象的句柄。
第2个参数pDutTime和第3个参数lPeriod要联合使用,pDutTime是一个LAGRE_INTEGER结构指针,指明了第一次触发的时间,时间格式是UTC(标准时间),是一个绝对值,如果要设置一个相对值,即让等待定时器在调用SetWaitableTimer函数之后多少时间发出第一次触发,只要传递一个负数给该参数即可,但是该数值必须是100ns的倍数,即单位是100ns,下面会举例说明。
第3个参数指明了以后触发的时间间隔,以毫秒为单位,该参数为0时,表示只有第一次的触发,以后没有触发。
第4和第5这两个参数与APC(异步过程调用)有关,这里不讨论。
最后一个参数bResume支持计算机暂停和恢复,一般传递FALSE。当它为TRUE的时候,当定时器触发的时候,如果此时计算机处于暂停状态,它会使计算机脱离暂停状态,并唤醒等待在该等待定时器上的线程。如果它为FALSE,如果此时计算机处于暂停状态,那么当该定时器触发的时候,等待在该等待定时器上的线程会被唤醒,但是要等待计算机恢复运行之后才能得到CPU时间。
比如,下面代码使用等待定时器让它在2008年8月8日晚上8:00开始触发。然后每隔1天触发。
HANDLE
SYSTEMTIME
FILETIME
LARGE_INTEGER
//
hTimer
//设置第一次触发时间
st.wYear
st.wMonth
st.wDayOfWeek
st.wDay
st.wHour
st.wMinute
st.wSecond
st.wMilliseconds
//将SYSTIME结构转换为FILETIME结构
SystemTimeToFileTime(&st,
//将本地时间转换为标准时间(UTC),SetWaitableTimer函数接受一个标准时间
LocalFileTimeToFileTime(&ftLocal,
//
liUTC.LowPart
liUTC.HighPart
//
SetWaitableTimer(hTimer,
当通过SetWaitTimer函数设置了一个等待定时器的属性之后,你可以通过CancelWaitableTimer函数来取消这些设置:
BOOL
5
“信号量内核对象”用于对资源进行计数。
在信号量内核对象内部,和其他内核对象一样,有一个使用计数,该使用计数表示信号量内核对象被打开的次数。
信号量内核对象中还有两个比较重要的数据,分别表示最大资源数和当前资源数。最大资源数表示能够管理的资源的总数,当前资源数表示目前可以被使用的资源数量。
可以使用CreateSeamphore函数来创建一个信号量内核对象,该函数成功返回句柄,失败返回NULL。
HANDLE
同样,可以打开一个指定名称的信号量,使用OpenSemaphore函数:
HANDLE
假如,作为一个服务器,有一个缓冲区需要用来存放客户的连接请求,还有一个线程池用来处理连接。但是该缓冲区和线程池的大小有限,比如至多只能同时接纳和处理5位客户的连接请求,而当有5位客户请求连接而尚未处理完成的时候,此时一个新客户也试图建立连接,那么这个连接过程应该被推后,直到有一个连接处理完成之后,这个新客户的连接才能被处理。
这个时候,可以使用信号量机制来处理线程同步的问题。
当服务器初始化的时候,最大资源数为5,没有任何服务器请求连接,可以使用如下代码创建信号量内核对象:
HANDLE
该函数创建了一个信号量内核对象,最大资源数为5,当前可用资源数为0。由于当前可用资源数为0,所以调用WaitForSingleObject等这些等待函数来等待该信号量句柄的线程都会进入等待状态。
这些等待函数在内部会查看信号量内核对象的可用资源数,如果该值大于0,则将其减1,线程保持可调度状态,这些比较和设置可用资源数是以原子过程进行的,所以是线程安全的。
如果可用资源数等于0,则线程进入等待状态,当一个线程将信号量的可用资源数递增之后,某个或某些等待的线程就可以进入就绪状态。
可以调用ReleaseSemaphore函数来让信号量内核对象的可用资源数递增:
BOOL
6
互斥内核对象的行为特征和关键代码段有点类似,但是它是属于内核对象,而关键代码段是用户模式对象,这导致了互斥内核对象的运行速度比关键代码段要低。所以,在考虑线程同步问题的时候,首先考虑用户模式的对象。
但是,互斥内核对象可以跨进程使用,当需要实现多进程之间的线程同步,就可用考虑使用互斥内核对象。而这点,关键代码段无能为力。
1、使用计数:表明该互斥内核对象被打开的次数。
2、线程ID
3、递归计数器
线程ID表明了该互斥内核对象被哪个线程所拥有,递归计数器表明了这个线程(拥有互斥对象)拥有这个互斥对象的次数。
互斥对象的使用规则如下
·
·
·
要使用互斥内核对象,首先必须创建它:
HANDLE
你通过“名字”来可以打开一个已经创建了的互斥内核对象:
HANDLE
创建了一个互斥内核对象,得到了它的句柄之后,就可以让它保护资源了。
一个线程中(下面用T表示),在你需要访问资源之前,可以先调用“等待函数”,传递该互斥对象(下面用M表示)的句柄该这些等待函数,在等待函数内部,通过句柄查看M的线程ID,如果不为0,表明M处于“未触发”状态,线程T进入等待状态。此时系统会记住这个情况,当M被其他线程释放,它的线程ID重新被设置为0的时候,系统会将一个等待在它上面的线程(比如T)的ID设置为M的线程ID,同时将M的递归计数器设置为1,允许该线程(比如T)进入可调度状态。
注意,对于互斥对象的线程ID的比较和设置都是以“原子”的形式进行的,所以互斥内核对象是“线程安全”的。
下面来讲那个例外的情况,这就是互斥内核对象允许以不正常的规则进行使用。也就是在一个互斥内核对象处于“未触发”状态的时候,一个等待在它上面的线程“或许”可以继续运行。
比如当前有一个处于“未触发状态”的互斥内核对象M,一个线程T(ID为X)。T调用等待函数等待M,这种情况下,通常T会进入等待状态。但是,系统查看T的ID和M的线程ID相同,都是X的情况下,线程并不会进入等待状态,而是保持在可调度状态。在线程成功等待互斥内核对象之后,互斥内核对象M的递归计数器加1。
当前线程如果对资源访问结束,必须释放互斥内核对象,使用ReleaseMutex函数:
BOOL
该函数将互斥内核对象的递归计数减1。如果一个线程多次成功地等待一个互斥内核对象,就要同样以相同的次数调用ReleaseMutex函数,从而递减其递归计数,当互斥内核对象的递归计数减为0后,其线程ID被设置为0,进入“已触发”状态。
当这个互斥内核对象进入“已触发”状态之时,系统查看当前是否有线程等待它,如果有,就以公平的原则选择其中一个线程,将这个互斥内核对象的线程ID设置为这个选中的线程的线程ID,互斥对象的递归计数被设置为1。
综合上面所叙述的,可以总结出,互斥内核对象不同于其他内核对象,就是它有一个“线程所有权”的概念,这就使得互斥内核对象比较特殊。
一个线程调用ReleaseMutex函数释放一个互斥对象,这时系统查看互斥对象的线程ID和这个线程的线程ID是否相同,如果相同,互斥对象的递归计数减1;否则ReleaseMutex不做任何工作,返回FALSE。
还有一种现象,称做“互斥对象被遗弃”。
假设一个互斥内核对象为一个线程所拥有,而这个线程却因为某些特殊的原因在终止,比如调用了ExitThread或TerminateThread函数,但是它在终止之前没有释放这个互斥对象。这个时候,系统能够跟踪拥有互斥内核对象的线程内核对象,系统知道这个互斥对象被一个线程遗弃了,就将互斥对象的线程ID设置为0,