您的位置:首页 > 编程语言

《Windows核心编程》读书笔记(六) 第9章 线程与内核对象的同步

2018-01-21 18:45 417 查看
第9章 线程与内核对象的同步
用户方式同步的优点是它的同步速度非常快。如果强调线程的运行速度,那么首先应该确定用户方式的线程同步机制是否适合需要

 

互锁函数家族只能在单值上运行,根本无法使线程进入等待状态。可以使用关键代码段使线程进入等待状态,但是只能用这些代码段对单个进程中的线程实施同步。还有,使用关键代码段时,很容易陷入死锁状态,因为在等待进入关键代码段时无法设定超时值。

 

内核对象机制的适应性远远优于用户方式机制。实际上,内核对象机制的唯一不足之处是它的速度比较慢。

 

下面的内核对象可以处于已通知状态或未通知状态:
进程,文件修改通知,线程,事件,作业,可等待定时器,文件,信标,控制台输入,互斥对象

线程可以使自己进入等待状态,直到一个对象变为已通知状态。注意,用于控制每个对象的已通知/未通知状态的规则要根据对象的类型而定。



等待函数

等待函数可使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态为止。这些等待函数中最常用的是WaitForSingleObject:

DWORD WaitForSingleObject(
HANDLE hObject,
DWORD dwMilliseconds );


第一个参数hObject标识一个能够支持被通知/未通知的内核对象(前面列出的任何一种对象都适用)。第二个参数dwMilliseconds允许该线程指明,为了等待该对象变为已通知状态,它将等待多长时间。

WaitForSingleObject的返回值能够指明调用线程为什么再次变为可调度状态。如果线程等待的对象变为已通知状态,那么返回值是WAIT_OBJECT_0。如果设置的超时已经到期,则返回值是WAIT_TIMEOUT。如果将一个错误的值(如一个无效句柄)传递给WaitForSingleObject,那么返回值将是WAIT_FAILED(若要了解详细信息,可调用GetLastError)。

WaitForMultipleObjects函数,它允许调用线程同时查看若干个内核对象的已通知状态:

 

WINBASEAPI
DWORD
WINAPI
WaitForMultipleObjects(
_In_ DWORD nCount,
_In_reads_(nCount) CONST HANDLE *lpHandles,
_In_ BOOL bWaitAll,
_In_ DWORD dwMilliseconds
);


dwCount参数用于指明想要让函数查看的内核对象的数量。这个值必须在1与MAXIMUM_WAIT_OBJECTS(在Windows头文件中定义为64)之间

一种方式是让线程进入等待状态,直到指定内核对象中的任何一个变为已通知状态。另一种方式是让线程进入等待状态,直到所有指定的内核对象都变为已通知状态。fWaitAll参数告诉该函数,你想要让它使用何种方式。如果为该参数传递TRUE,那么在所有对象变为已通知状态之前,该函数将不允许调用线程运行。

 

可能想要知道哪个对象变为已通知状态。返回值是WAIT_OBJECT_0与(WAIT_OBJECT_0+dwCount-1)之间的一个值。换句话说,如果返回值不是WAIT_TIMEOUT,也不是WAIT_FAILED,那么应该从返回值中减去WAIT_OBJECT_0。产生的数字是作为第二个参数传递给WaitForMultipleObjects的句柄数组中的索引。该索引说明哪个对象变为已通知状态。

 

成功等待的副作用

对于有些内核对象来说,成功地调用WaitForSingleObject和WaitForMultipleObjects,实际上会改变对象的状态。成功地调用是指函数发现对象已经得到通知并且返回一个相对于WAIT_OBJECT_0的值。如果函数返回WAIT_TIMEOUT或WAIT_FAILED,那么调用就没有成功。如果函数调用没有成功,对象的状态就不可能改变。

 

当一个对象的状态改变时,我称之为成功等待的副作用。例如,有一个线程正在等待自动清除事件对象(本章后面将要介绍)。当事件对象变为已通知状态时,函数就会发现这个情况,并将WAIT_OBJECT_0返回给调用线程。但是就在函数返回之前,该事件将被置为未通知状态,这就是成功等待的副作用。

 

其他对象拥有不同的副作用,而有些对象则根本没有任何副作用。进程和线程内核对象就根本没有任何副作用,也就是说,在这些对象之一上进行等待决不会改变对象的状态。

 

究竟是什么原因使得WaitForMultipleObjects函数如此有用呢,因为它能够以原子操作方式来执行它的所有操作。当一个线程调用WaitForMultipleObjects函数时,该函数能够测试所有对象的通知状态,并且能够将所有必要的副作用作为一项操作来执行。

 

有一个重要问题必须注意,即WaitForMultipleObjects是以原子操作方式运行的。当它检查内核对象的状态时,其他任何线程都无法背着对象改变它的状态。这可以防止出现死锁情况。

 

如果多个线程等待单个内核对象,那么当该对象变成已通知状态时,系统究竟决定唤醒哪个线程呢?Microsoft对这个问题的正式回答是:“算法是公平的。”在实际操作中,Microsoft使用的算法是常用的“先进先出”的方案。

 

事件内核对象

在所有的内核对象中,事件内核对象是个最基本的对象。它们包含一个使用计数(与所有内核对象一样),一个用于指明该事件是个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。

 

事件能够通知一个操作已经完成。有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。

WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateEventW(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCWSTR lpName
);


系统创建事件对象后,createEvent就将与进程相关的句柄返回给事件对象。其他进程中的线程可以获得对该对象的访问权,方法是使用在pszName参数中传递的相同值,使用继承性,使用DuplicateHandle函数等来调用CreateEvent,或者调用OpenEvent,在pszName参数中设定一个与调用CreateEvent时设定的名字相匹配的名字

HANDLE OpenEvent(
DWORD fdwAccess,
BOOL fInherit,
PCTSTR pszName);


当不再需要事件内核对象时,应该调用CloseHandle函数。
一旦事件已经创建,就可以直接控制它的状态。当调用SetEvent时,可以将事件改为已通知状态:

BOOL SetEvent(HANDLE hEvent);


当调用ResetEvent函数时,可以将该事件改为未通知状态:

BOOL ResetEvent(HANDLE hEvent);

Microsoft为自动重置的事件定义了应该成功等待的副作用规则,即当线程成功地等待到该对象时,自动重置的事件就会自动重置到未通知状态。

 

等待定时器内核对象

等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。它们通常用来在某个时间执行某个操作。

若要创建等待定时器,只需要调用CreateWaitableTimer函数:

HANDLE CreateWaitableTimer(
PCRITICAL_SECTION psa,
BOOL fManualReset,
PCTSTR pszName);

进程可以获得它自己的与进程相关的现有等待定时器的句柄,方法是调用OpenWaitableTimer函数

HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandel,
PCTSTR pszName)


定时器的松散特性

定时器常常用于通信协议中。如果你客户机为每个对服务器的请求创建一个定时器内核对象,那么系统的运行性能就会受到影响。

凡是称职的Windows编程员都会立即将等待定时器与用户定时器(用SetTimer函数进行设置)进行比较。用户定时器需要在应用程序中设置许多附加的用户界面结构,这使定时器变得资源更加密集。等待定时器属于内核对象,这意味着它们可以供多个线程共享,并且是安全的。

 

运用等待定时器,当到了规定时间的时候,更有可能得到通知。WM_TIMER消息始终属于最低优先级的消息,当线程的队列中没有其他消息时,才检索该消息。

 

信标内核对象

信标内核对象用于对资源进行计数。它们与所有内核对象一样,包含一个使用数量,但是它们也包含另外两个带符号的32位值,一个是最大资源数量,一个是当前资源数量。最大资源数量用于标识信标能够控制的资源的最大数量,而当前资源数量则用于标识当前可以使用的资源的数量。

信标的使用规则如下:
•如果当前资源的数量大于0,则发出信标信号。
•如果当前资源数量是0,则不发出信标信号。
•系统决不允许当前资源的数量为负值。
•当前资源数量决不能大于最大资源数量。

HANDLE CreateSemaphore(
PCRITICAL_SECTION psa,
LONG lInitialCount,	// lInitialCount参数用于指明开始时(当前)这些资源中有多少可供使用。
LONG lMaximumCount,  //lMaximumCount参数用于告诉系统,应用程序处理的最大资源数量是多少。
PCTSTR pszName);


通过调用OpenSemaphore函数,另一个进程可以获得它自己的进程与现有信标相关的句柄:

 

HANDLE OpenSemaphore(
DWORD fdwAccess,
BOOL bInheritHandel,
PCTSTR pszName);


信标的出色之处在于它们能够以原子操作方式来执行测试和设置操作,这就是说,当向信标申请一个资源时,操作系统就要检查是否有这个资源可供使用,同时将可用资源的数量递减,而不让另一个线程加以干扰。只有当资源数量递减后,系统才允许另一个线程申请对资源的访问权。

 

如果该等待函数确定信标的当前资源数量是0(信标没有发出通知信号),那么系统就调用函数进入等待状态。当另一个线程将对信标的当前资源数量进行递增时,系统会记住该等待线程(或多个线程),并允许它变为可调度状态(相应地递减它的当前资源数量)。通过调用ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增:

BOOL ReleaseSemaphore(
HANDLE hsem,
LONG lReleaseCount,
PLONG plPreviousCount);

函数只是将lReleaseCount中的值添加给信标的当前资源数量。通常情况下,为lReleaseCount参数传递1,但是,不一定非要传递这个值。我常常传递2或更大的值。该函数也能够在它的*plPreviousCount中返回当前资源数量的原始值。实际上几乎没有应用程序关心这个值,因此可以传递NULL,将它忽略。

 

互斥对象内核对象

互斥对象(mutex)内核对象能够确保线程拥有对单个资源的互斥访问权。实际上互斥对象是因此而得名的。互斥对象包含一个使用数量,一个线程ID和一个递归计数器。互斥对象的行为特性与关键代码段相同,但是互斥对象属于内核对象,而关键代码段则属于用户方式对象。这意味着互斥对象的运行速度比关键代码段要慢。但是这也意味着不同进程中的多个线程能够

访问单个互斥对象,并且这意味着线程在等待访问资源时可以设定一个超时值。

 

ID用于标识系统中的哪个线程当前拥有互斥对象,递归计数器用于指明该线程拥有互斥对象的次数。互斥对象有许多用途,属于最常用的内核对象之一。通常来说,它们用于保护由多个线程访问的内存块。如果多个线程要同时访问内存块,内存块中的数据就可能遭到破坏。互斥对象能够保证访问内存块的任何线程拥有对该内存块的独占访问权,这样就能够保证数据的完整性。

 

互斥对象的使用规则如下:
•如果线程ID是0(这是个无效ID),互斥对象不被任何线程所拥有,并且发出该互斥对象的通知信号。
•如果ID是个非0数字,那么一个线程就拥有互斥对象,并且不发出该互斥对象的通知信号。
•与所有其他内核对象不同,互斥对象在操作系统中拥有特殊的代码,允许它们违反正常的规则(后面将要介绍这个异常情况)。

若要使用互斥对象,必须有一个进程首先调用CreateMutex,以便创建互斥对象:

HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL fInitialOwner,
PCTSTR pszName)


fInitialOwner参数用于控制互斥对象的初始状态。如果传递FALSE(这是通常情况下传递的值),那么互斥对象的ID和递归计数器均被设置为0。这意味着该互斥对象没有被任何线程所拥有,因此要发出它的通知信号。

如果为fInitialOwner参数传递TRUE,那么该对象的线程ID被设置为调用线程的ID,递归计数器被设置为1。由于ID是个非0数字,因此该互斥对象开始时不发出通知信号。

       通过调用一个等待函数,并传递负责保护资源的互斥对象的句柄,线程就能够获得对共享资源的访问权。在内部,等待函数要检查线程的ID,以了解它是否是0(互斥对象发出通知信号)。如果线程ID是0,那么该线程ID被设置为调用线程的ID,递归计数器被设置为1,同时,调用线程保持可调度状态。如果等待函数发现ID不是0(不发出互斥对象的通知信号),那么调用线程便进入等待状态。系统将记住这个情况,并且在互斥对象的ID重新设置为0时,将线程ID设置为等待线程的ID,将递归计数器设置为1,并且允许等待线程再次成为可调度线程。与所有情况一样,对互斥内核对象进行的检查和修改都是以原子操作方式进行的。

通过调用OpenMutex,另一个进程可以获得它自己进程与现有互斥对象相关的句柄

一旦线程成功地等待到一个互斥对象,该线程就知道它已经拥有对受保护资源的独占访问权。试图访问该资源的任何其他线程(通过等待相同的互斥对象)均被置于等待状态中。当目前拥有对资源的访问权的线程不再需要它的访问权时,它必须调用ReleaseMutex函数来释放该互斥对象

对于互斥对象来说,正常的内核对象的已通知和未通知规则存在一个特殊的异常情况。比如说,一个线程试图等待一个未通知的互斥对象。在这种情况下,该线程通常被置于等待状态。然而,系统要查看试图获取互斥对象的线程的ID是否与互斥对象中记录的线程ID相同。如果两个线程ID相同,即使互斥对象处于未通知状态,系统也允许该线程保持可调度状态。

 

释放问题

没有一种对象能够记住哪个线程成功地等待到该对象,只有互斥对象能够对此保持跟踪。互斥对象的线程所有权概念是互斥对象为什么会拥有特殊异常规则的原因,这个异常规则使得线程能够获取该互斥对象,尽管它没有发出通知。

 

如果在释放互斥对象之前,拥有互斥对象的线程终止运行(使用ExitThread、TerminateThread、ExitProcess或TerminateProcess函数),那么互斥对象和正在等待互斥对象的其他线程将会发生什么情况呢?答案是,系统将把该互斥对象视为已经被放弃——拥有互斥对象的线程决不会释放它,因为该线程已经终止运行。

 

当一个互斥对象被放弃时,系统将自动把互斥对象的ID复置为0,并将它的递归计数器复置为0。这与前面的情况相同,差别在于等待函数并不将通常的WAIT_OBJECT_0值返回给线程。相反,等待函数返回的是特殊的WAIT_ABANDONED值。这个特殊的返回值(它只适用于互斥对象)用于指明线程正在等待的互斥对象是由另一个线程拥有的,而这另一个线程已经在它完成对共享资源的使用前终止运行。这不是可以进入的最佳情况。



线程同步对象速查表





其他的线程同步函数

异步设备I/O

异步设备I/O使得线程能够启动一个读操作或写操作,但是不必等待读操作或写操作完成。

SingleObjectAndWait

在单个原子方式的操作中发出关于内核对象的通知并等待另一个内核对象

 

DWORD SignalObjectAndWait(
HANDLE hObjectToSignal,
HANDLE hObjectToWatiOn,
DWORD dwMilliseconds,
BOOL fAlertable
);


该函数是对Windows的令人欢迎的一个补充,原因有二。首先,因为常常需要通知一个对象,等待另一个对象,用一个函数来执行两个操作可以节省许多处理时间。每次调用一个函数,使线程从用户方式代码变成内核方式代码,大约需要运行1000个CPU周期。第二,如果没有SignalObjectAndWait函数,一个线程将无法知道另一个线程何时处于等待状态。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: