用内核对象进行线程同步

内核对象:Windows操作系统使用内核对象来管理进程、线程、文件等诸多种类的大量资源。内核对象的创建通常是通过Windows API,比如CreateThread将创建一个线程内核对象,并返回一个内核对象句柄。内核对象实际上是一小块内存,其中包括了引用计数、安全性描述等信息,操作系统通过这一小段内存来管理对应的内核资源。内核对象的实际内存地址并非句柄所展示的,它们在进程内的内核对象句柄表中有映射。

在前几篇中,介绍了在用户模式下的线程同步机制:www.189works.com InterLocked系列、关键段、Slim读写锁。这些同步机制可以在进行线程同步的同时让线程保持在用户模式下。然而用户模式下的线程同步机制有时不能满足我们的要求。从这篇开始,将介绍使用内核对象进行线程同步。在考虑是使用用户模式的同步机制还是使用内核对象来同步的时候,需要综合考量,尽量使用用户模式的线程同步机制。

内核对象普遍存在两种状态,要么是触发,要么是未触发。每种内核对象在这两个状态间切换过程都有其特殊的特点,比如进程内核对象在创建的时候总是处于未触发状态,当进程终止时,会变成触发状态;而且进程内核对象永远不会从触发态变回未触发态。于是,如果我们的线程需要等待子进程终止时才继续,那么可以将线程进入等待状态,当子进程标识的进程内核对象变成触发状态的时候唤醒线程!我们所需要的仅仅是Windows为我们提供的等待函数。

 

等待函数

等待函数能够是一个线程进入等待状态,直到指定的内核对象被触发为止。这些等待函数有:

DWORD WINAPI WaitForSingleObject(  __in  HANDLE hHandle,  __in  DWORD dwMilliseconds);

当线程调用WaitForSingleObject的时候,第一个参数hObject用来标识内核对象,第二个参数是个超时时间(传入INFINITE表示永远等待直到触发)。

DWORD WINAPI WaitForMultipleObjects(  __in  DWORD nCount,  __in  const HANDLE *lpHandles,  __in  BOOL bWaitAll,  __in  DWORD dwMilliseconds);www.189works.com

WaitForMultipleObjects允许调用线程检查多个内核对象的触发状态。

使用等待函数有时是会改变内核对象的状态的。比如:线程正在等待一个自动重置事件对象,当事件对象被触发的时候,函数会检测到这一情况并返回调用线程,但是在返回之前,他会使事件变为非触发状态。等待函数在不同的内核对象上调用并返回所引起的这样类似的“副作用”是不同的。在对内核对象展开介绍后,将看到这一点。

如果多个线程在同时等待同一个内核对象,那么任何一个线程都有可能被唤醒。我们不应该做出类似“谁先等待谁就先唤醒”这样的假设。

接下来,将分几篇的内容分别介绍那些与线程同步有关的内核对象。

 

事件内核对象

事件内核对象分为手动重置和自动重置,区别在于当对象从未触发状态变成触发状态后,会不会自动重置回未触发状态。与其他内核对象相同,事件内核对象也包括引用计数。下面的函数CreateEvent用以创建事件内核对象:

HANDLE CreateEvent(  LPSECURITY_ATTRIBUTES lpEventAttributes,   BOOL bManualReset,   BOOL bInitialState,   LPTSTR lpName ); 

lpEventAttributes:会被忽略,必须为NULL(这里是MSDN的说法,在Windows Via C/C++中,暗示了这个参数与内核对象的安全属性和共享特性有关。不过通常都会传入NULL,因为不太会考虑让一个事件对象变为可继承)

bManualReset:手动重置(TRUE)还是自动重置(FALSE)

bInitialState:初始状态为触发(TRUE)还是非触发(FALSE)

lpName:用于共享内核对象的机制,这里与线程同步无关,不再阐述,传入NULL即可。

返回值:事件对象的句柄

Windows Vista还支持下面这个函数CreateEventEx来创建事件内核对象,详情请参见MSDN:

HANDLE WINAPI CreateEventEx(  __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,  __in_opt  LPCTSTR lpName,  __in      DWORD dwFlags,  __in      DWORD dwDesiredAccess);

 

我们使用下面的函数来控制事件对象的状态:

BOOL SetEvent(  HANDLE hEvent ); //将内核对象设置为触发状态BOOL ResetEvent(   HANDLE hEvent );//将内核对象设置为非触发状态BOOL PulseEvent(  HANDLE hEvent ); //触发内核对象并立即将其重置为未触发状态,会唤醒正在等待的线程

如果用图来表示他们的作用就很直观了:

image

另外OpenEvent通常用于进程同步,但前提是事件内核对象必须有别名(通过给某些内核对象起别名,是一种共享内核对象的方式):

HANDLE OpenEvent(   DWORD dwDesiredAccess,   BOOL bInheritHandle,   LPCTSTR lpName ); 

 

下图描述了事件内核对象的示例用法(同一进程内):

image

主线程先创建一个事件内核对象,并创建两个线程,这两个线程的执行需要依赖主线程的准备工作,因此调用WaitForSingleObject等待事件对象触发。主线程完成准备工作后,调用SetEvent使对象变成触发状态,这时两个子线程将被唤醒开始执行代码。

在后面的篇章中,将继续介绍其他可以用来线程同步的内核对象。