《Windows via C/C++》学习笔记(五) 内核对象的“线程同步”

《Windows via C/C++》学习笔记 —— 内核对象的“线程同步”之“等待函数”

用户模式的线程同步机制效率高,如果需要考虑线程同步问题,应该首先考虑用户模式的线程同步方法。

  但是,用户模式的线程同步有限制,对于多个进程之间的线程同步,用户模式的线程同步方法无能为力。这时,只能考虑使用内核模式。

 

  Windows提供了许多内核对象来实现线程的同步。对于线程同步而言,这些内核对象有两个非常重要的状态:“已通知”状态,“未通知”状态(也有翻译为:受信状态,未受信状态)。Windows提供了几种内核对象可以处于已通知状态和未通知状态:进程、线程、作业、文件、控制台输入/输出/错误流、事件、等待定时器、信号量、互斥对象。

 

  你可以通知一个内核对象,使之处于“已通知状态”,然后让其他等待在该内核对象上的线程继续执行。你可以使用Windows提供的API函数,等待函数来等待某一个或某些内核对象变为已通知状态。

  你可以使用WaitForSingleObject函数来等待一个内核对象变为已通知状态:

DWORD WaitForSingleObject(
   HANDLE hObject,     
// 指明一个内核对象的句柄
   DWORD dwMilliseconds);      // 等待时间

 

  该函数需要传递一个内核对象句柄,该句柄标识一个内核对象,如果该内核对象处于未通知状态,则该函数导致线程进入阻塞状态;如果该内核对象处于已通知状态,则该函数立即返回WAIT_OBJECT_0。第二个参数指明了需要等待的时间(毫秒),可以传递INFINITE指明要无限期等待下去。如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED。可以通过下面的代码来判断:

 

复制代码
DWORD dw  =  WaitForSingleObject(hProcess,  5000 );  // 等待一个进程结束
switch  (dw)
{
   
case  WAIT_OBJECT_0:
      
//  hProcess所代表的进程在5秒内结束
        break ;

   
case  WAIT_TIMEOUT:
      
//  等待时间超过5秒
        break ;

   
case  WAIT_FAILED:
      
//  函数调用失败,比如传递了一个无效的句柄
        break ;
}
复制代码

 

  还可以使用WaitForMulitpleObjects函数来等待多个内核对象变为已通知状态:

DWORD WaitForMultipleObjects(
   DWORD dwCount,     
// 等待的内核对象个数
   CONST HANDLE *  phObjects,      // 一个存放被等待的内核对象句柄的数组
   BOOL bWaitAll,      // 是否等到所有内核对象为已通知状态后才返回
   DWORD dwMilliseconds);      // 等待时间

 

  该函数的第一个参数指明等待的内核对象的个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一个值。phObjects参数是一个存放等待的内核对象句柄的数组。bWaitAll参数如果为TRUE,则只有当等待的所有内核对象为已通知状态时函数才返回,如果为FALSE,则只要一个内核对象为已通知状态,则该函数返回。第四个参数和WaitForSingleObject中的dwMilliseconds参数类似。

  该函数失败,返回WAIT_FAILED;如果超时,返回WAIT_TIMEOUT;如果bWaitAll参数为TRUE,函数成功则返回WAIT_OBJECT_0,如果bWaitAll为FALSE,函数成功则返回值指明是哪个内核对象收到通知。

  可以如下使用该函数:

 

复制代码
HANDLE h[ 3 ];      // 句柄数组

// 三个进程句柄
h[ 0 =  hProcess1;
h[
1 =  hProcess2;
h[
2 =  hProcess3;

DWORD dw 
=  WaitForMultipleObjects( 3 , h, FALSE,  5000 );  // 等待3个进程结束

switch  (dw)
{
   
case  WAIT_FAILED:
      
//  函数呼叫失败
        break ;

   
case  WAIT_TIMEOUT:
      
//  超时
        break ;

   
case  WAIT_OBJECT_0  +   0 :
      
//  h[0](hProcess1)所代表的进程结束
        break ;

   
case  WAIT_OBJECT_0  +   1 :
      
//  h[1](hProcess2)所代表的进程结束
        break ;

   
case  WAIT_OBJECT_0  +   2 :
      
//  h[2](hProcess3)所代表的进程结束
        break ;
}
复制代码

 

  你也可以同时通知一个内核对象,同时等待另一个内核对象,这两个操作以原子的方式进行:

DWORD SignalObjectAndWait(
   HANDLE hObjectToSignal,   
// 通知的内核对象
   HANDLE hObjectToWaitOn,    // 等待的内核对象
   DWORD dwMilliseconds,      // 等待的时间
   BOOL bAlertable);          // 与IO完成端口有关的参数,暂不讨论

 

  该函数在内部使得hObjectToSignal参数所指明的内核对象变成已通知状态,同时等待hObjectToWaitOn参数所代表的内核对象。dwMilliseconds参数的用法与WaitForSingleObject函数类似。
  该函数返回如下:WAIT_OBJECT_0,WAIT_TIMEOUT,WAIT_FAILED,WAIT_IO_COMPLETION。

  等你需要通知一个互斥内核对象并等待一个事件内核对象的时候,可以这么写:

ReleaseMutex(hMutex);
WaitForSingleObject(hEvent, INFINITE);

  可是,这样的代码不是以原子的方式来操纵这两个内核对象。因此,可以更改如下:

SignalObjectAndWait(hMutex, hEvent, INFINITE, FALSE);

 

《Windows via C/C++》学习笔记 —— 内核对象的“线程同步”之“事件内核对象”

本书首先介绍了一个重要的概念“成功的副作用”,这里笔者作一下简述。

  当调用WaitForSingleObject和WaitForMultipleObject函数成功之后,该函数在返回成功的时候,系统可能会自动更改所等待的内核对象的状态,即将其从“已通知状态”切换为“未通知状态”。

  当一个内核对象的状态被更改,称之为“成功等待的副作用”。比如,一个“自动重置”的事件内核对象,当调用等待函数成功返回的时候,该事件内核对象会由已通知状态转变为未通知状态。

  比如此时有一个自动重置的事件内核对象hEvent,它处于未通知状态。线程T1、T2、T3内部调用“WaitForSingleObject(hEvent, INFINITE);”,这样当该事件内核对象变为“已通知”状态的话,T1线程“可能”被唤醒,但是其他的线程T2和T3呢?由于在T1线程内部WaitForSingleObject函数返回成功,又将hEvent事件内核对象设置为“未通知”状态,那么T2和T3就不可能被唤醒。

  也就是说,“成功等待的副作用”会导致多个等待在同一个内核对象上的线程只能被唤醒一个

 

  好,下面我们来讨论“事件内核对象”。

  在所有内核对象中,事件内核对象是最基本的一个内核对象。在事件内核对象内部,有以下几个比较重要的数据:

1、有一个“引用计数”:指明被打开的次数;

2、一个“布尔值”:指明该事件内核对象是自动重置的还是人工重置的;

3、另一个“布尔值”:指明该事件内核对象是“已通知状态”还是“未通知状态”。

  事件内核对象可以通知一个事件已经完成。有两种不同的类型:自动重置和人工重置。当人工重置的事件内核对象得到通知的时候,所有等待在事件内核对象上的线程都变成可调度线程。当一个自动重置的事件内核对象得到通知的时候,等待在该事件内核对象上的线程只有一个能变成可调度状态。

 

  要使用事件内核对象,首先调用CreateEvent函数来创建一个事件内核对象:

HANDLE CreateEvent(
   PSECURITY_ATTRIBUTES psa,
   BOOL bManualReset,
   BOOL bInitialState,
   PCTSTR pszName);

 

  参数psa是一个SECURITY_ATTRIBUTES(安全属性)结构的指针,一般设置为默认安全,传递NULL。

  bManualReset参数指定了该内核对象是人工重置(传递TRUE)的还是自动重置(传递FALSE)的。

  bInitialState参数指定了该内核对象起始状态是已通知(传递TRUE)还是未通知状态(FALSE)。

  pszName参数为要创建的事件内核对象起一个名字,如果传递NULL,则创建一个“匿名”的事件内核对象。如果不传递NULL,且系统中已经存在该名字的事件内核对象,则不创建新的事件内核对象而是打开这个已经存在的,返回它的句柄。

  该函数如果成功,返回事件内核对象的句柄,这样就可以操纵它了。如果失败,返回NULL。

 

  Windows Vista提供了另一个函数来创建事件内核对象:

HANDLE CreateEventEx(
   PSECURITY_ATTRIBUTES psa,
   PCTSTR pszName,
   DWORD dwFlags,
   DWORD dwDesiredAccess);

 

  该函数的psa和pszName参数的意义和函数CreateEvent相同。

  参数dwFlags可以有以下数据的“位或组合”:

WinBase.h中定义的位组合数据 

描述 

CREATE_EVENT_INITIAL_SET(0x00000002)

如果设置了该数据,则表明事件内核对象的起始状态为已通知状态;否则起始状态为未通知状态。

CREATE_EVENT_MANUAL_RESET(0x00000001)

如果设置了该数据,则表明事件内核对象是人工重置的;否则为自动重置的。

  参数dwDesiredAccess可以让你对该事件内核对象的访问加一些限制,本书没有细说,查MSDN就可以了吧。

 

  可以打开一个“命名”的事件内核对象:

HANDLE OpenEvent(
   DWORD dwDesiredAccess,
   BOOL bInherit,
   PCTSTR pszName);

 

  第一个参数指明的访问的限制,第二个参数表示该事件内核对象的句柄能够被子进程继承,第三个参数指明了该事件内核对象的名字。该函数成功返回事件内核对象的句柄,失败返回NULL。

  当不需要使用这些句柄时,需要调用CloseHandle函数来递减内核对象的引用计数,使得该内核对象可以被及时清除。

 

  当一个事件内核对象被创建之后,你可以直接控制它的状态。你可以通知它,使得它从未通知状态转变为已通知状态: 

BOOL SetEvent(HANDLE hEvent);

 

  也可以重新设置它,使它从已通知状态变为未通知状态:

BOOL ResetEvent(HANDLE hEvent);

 

  一个自动重置的事件内核对象,如果等待成功,由于“成功等待的副作用”机制会将该事件内核对象由已通知状态变为未通知状态,这个时候就没有必要调用ResetEvent函数了。

  如果是一个人工重置的事件内核对象,等待成功之后,并不会被设置为未通知状态,而是要程序员调用ResetRvent函数来使之转变为未通知状态。

 

  还有要注意的就是,一个“自动重置”的事件内核对象收到通知,转变为已通知状态的时候,最多只能唤醒“一个”等待在它上的线程。一个“人工重置”的事件内核对象收到通知,转变为已通知状态的时候,能够唤醒“所有”等待在它上的线程。

《Windows via C/C++》学习笔记 —— 内核对象的“线程同步”之“等待定时器”

等待定时器(waitable timer)是在某个时间或按规定的时间间隔通知自己的内核对象。可以把它理解为一个定时发送信号的东西。

  要创建一个等待定时器内核对象,可以调用函数CreateWaitableTimer。可以为该函数赋予不同的参数来指定一个定时器内核对象的属性。

HANDLE CreateWaitableTimer(
   PSECURITY_ATTRIBUTES psa,
   BOOL bManualReset,
   PCTSTR pszName);

 

  该函数第一个参数是安全属性结构指针。第三个参数是要创建的定时器内核对象名称。第二个参数指明了该定时器内核对象是人工重置(TRUE)的还是自动重置(FALSE)的。该函数成功,返回句柄,失败则返回NULL。

  当一个人工重置的定时器内核对象收到通知时,所有等待在该内核对象上的线程都可以被唤醒,进入就绪状态。一个自动重置的定时器内核对象收到通知时,只有一个等待在该内核对象上的线程可以被调度。

  当然,也可以打开一个特定名字的定时器内核对象,呼叫OpenWaitableTimer函数:

HANDLE OpenWaitableTimer(
   DWORD dwDesiredAccess,
   BOOL bInheritHandle,
   PCTSTR pszName);

 

  等待定时器内核对象创建的时候的状态总是“未通知状态”。你可以呼叫SetWaitableTimer函数来设定等待定时器内核对象何时获得通知。

复制代码
BOOL SetWaitableTimer(
   HANDLE hTimer,                   
// 等待定时器句柄
    const  LARGE_INTEGER  * pDueTime,    // 第一次通知的时刻(负数表示相对值)
   LONG lPeriod,                     // 以后通知的时间间隔(毫秒)
   PTIMERAPCROUTINE pfnCompletionRoutine,   // APC异步函数地址
   PVOID pvArgToCompletionRoutine,   // APC异步函数参数
   BOOL bResume);                    // 是否让计算机摆脱暂停状态
复制代码

 

  该函数的第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 hTimer;      // 等待定时器句柄
SYSTEMTIME st;      // SYSTEMTIME结构,用来设置第1次通知的时间
FILETIME ftLocal, ftUTC;  // FILETIME结构,用来接受STSTEMTIME结构的转换
LARGE_INTEGER liUTC;    // LARGE_INTEGER结构,作为SetWaitableTimer的参数

//  创建一个匿名的默认安全性的人工重置的等待定时器内核对象,并保存句柄
hTimer  =  CreateWaitableTimer(NULL, FALSE, NULL);

// 设置第一次通知时间
st.wYear          =   2008 //  年
st.wMonth         =   8 ;     //  月
st.wDayOfWeek     =   0 ;     //  一周中的某个星期
st.wDay           =   8 ;     //  日
st.wHour          =   20 ;    //  小时(下午8点)
st.wMinute        =   8 ;     //  分
st.wSecond        =   0 ;     //  秒
st.wMilliseconds  =   0 ;     //  毫秒

// 将SYSTIME结构转换为FILETIME结构
SystemTimeToFileTime( & st,  & ftLocal);

// 将本地时间转换为标准时间(UTC),SetWaitableTimer函数接受一个标准时间
LocalFileTimeToFileTime( & ftLocal,  & ftUTC);

//  设置LARGE_INTEGER结构,因为该结构数据要作为SetWaitableTimer的参数
liUTC.LowPart   =  ftUTC.dwLowDateTime;
liUTC.HighPart 
=  ftUTC.dwHighDateTime;

//  设置等待定时器内核对象(一天的毫秒数为24*60*60*1000)
SetWaitableTimer(hTimer,  & liUTC,  24   *   60   *   60   *   1000 ,
                 NULL, NULL, FALSE);
复制代码

 

  下面的代码创建了一个等待定时器,当调用SetWaitableTimer函数之后2秒会第一次通知,然后每隔1秒通知一次:

复制代码
HALDLE hTimer;
LARGE_INTEGER li;
hTimer 
=  CreateWaitableTime(NULL, FALSE, NULL);
const   int  nTimerUnitsPerSecond  =   100000000   /   100 // 每1s中有多少个100ns
li.QuadPart  =   - ( 2   *  nTimerUnitsPerSecond );    // 负数,表示相对值2秒
SetWaitableTimer(hTimer,  & li,  1000 , NULL, NULL, FALSE);
复制代码

 

  当通过SetWaitTimer函数设置了一个等待定时器的属性之后,你可以通过CancelWaitableTimer函数来取消这些设置:

BOOL CancelWaitableTimer(HANDLE hTimer);

 

  当你不再需要等待定时器的时候,通过调用CloseHanble函数关闭之

 

 

等待定时器与APC(异步过程调用)项排队:

 

  Windows允许在等待定时器的通知的时候,那些调用SetWaitTimer函数的线程的异步过程调用(APC)进行排队。

  要使用这个特性,需要在线程调用SetWaitTimer函数的时候,设置第4个参数pfnCompletionRoutine和第5的参数pvArgToCompletionRoutine。这个异步过程需要如下形式:

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,
                   DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
   
//  特定的任务
}

 

  该函数名TimerAPCRoutine可以任意。该函数可以在等待定时器收到通知的时候,由调用SetWaitableTimer函数的线程来调用,但是该线程必须处于“待命等待”状态。也就是说你的线程因为调用以下函数的而处于等待状态中:SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx,MsgForMultipleObjectEx,SingleObjectAndWait。如果该线程没有因为调用这些函数而进入等待状态,那么系统不会给定时器APC排队。

 

  下面讲一下详细的APC调用的过程:当你的等待定时器通知的时候,如果你的线程处于“待命等待”状态,那么系统就调用上面具有TimerAPCRoutine异步函数的格式的函数,该异步函数的第一个参数就是你传递给SetWaitableTimer函数的第5个参数pvArgToCompletionRoutine的值。其他两个参数用于指明定时器什么时候发出通知。

  下面的代码指明了使用等待定时器的正确方法:

复制代码
void  SomeFunc()
{
   
//  创建一个等待定时器(人工重置)
   HANDLE hTimer  =  CreateWaitableTimer(NULL, TRUE, NULL);

   
//  当调用SetWaitableTimer时候立刻通知等待定时器
   LARGE_INTEGER li  =  {  0  };
   SetWaitableTimer(hTimer, 
& li,  5000 , TimerAPCRoutine, NULL, FALSE);

   
//  线程进入“待命等待”状态,并无限期等待
   SleepEx(INFINITE, TRUE);

   CloseHandle(hTimer);   
// 关闭句柄
}
复制代码

 

  当所有的APC项都完成,即所有的异步函数都结束之后,等待的函数才会返回(比如SleepEx函数)。所以,必须确保等待定时器再次变为已通知之前,异步函数就完成了,这样,等待定时器的APC排队速度不会比它的处理速度慢。

 

  注意,当使用APC机制的时候,线程不能应该等待“等待定时器的句柄”,也不应该以待命等待的方式等待“等待定时的句柄”,下面的方法是错误的:

HANDLE hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

SetWaitableTimer(hTimer, &li, 2000, TimerAPCRoutine, NULL, FALSE);

WaitForSingleObjectEx(hTimer, INFINITE, TRUE);

 

  这段代码让线程2次等待一个等待定时器,一个是等待该等待定时器的句柄,还有一个是“待命等待”。当定时器变为已通知状态的时候,该等待就成功了,然后线程被唤醒,导致线程摆脱了“待命等待”状态,APC函数不会被调用。

 

  由于等待定时器的管理和重新设定是比较麻烦的,所以一般开发者很少使用这个机制,而是使用CreateThreadpoolTimer来创建线程池的定时器来处理问题。

  等待定时器的APC机制也往往被I/O完成端口所替代。

 

  最后,把“等待定时器”和“用户界面定时器”做一下比较。

  用户界面定时器是通过SetTimer函数设置的,定时器一般发送WM_TIMER消息给调用SetTimer函数的线程和窗口,因此只能有一个线程收到通知。而“人工重置”的等待定时器可以让多个线程同时收到通知。

  运用等待定时器,可以让你的线程到了规定的时间就收到通知。而用户界面定时器,发送的WM_TIMER消息属于最低优先级的消息,当线程队列中没有其他消息的时候才会检索该消息,因此可能会有一点延迟。

  另外,WM_TIMER消息的定时精度比较低,没有等待定时器那么高。

《Windows via C/C++》学习笔记 —— 内核对象的“线程同步”之“信号量”

“信号量内核对象”用于对资源进行计数。

  在信号量内核对象内部,和其他内核对象一样,有一个使用计数,该使用计数表示信号量内核对象被打开的次数。

  信号量内核对象中还有两个比较重要的数据,分别表示最大资源数和当前资源数。最大资源数表示能够管理的资源的总数,当前资源数表示目前可以被使用的资源数量。

 

  可以使用CreateSeamphore函数来创建一个信号量内核对象,该函数成功返回句柄,失败返回NULL。

HANDLE CreateSemaphore(
   PSECURITY_ATTRIBUTE psa,     
// 安全属性结构指针
   LONG lInitialCount,           // 初始可用资源数
   LONG lMaximumCount,           // 最大资源数
   PCTSTR pszName);              // 信号量内核对象的名字(NULL表示匿名)

 

  在Windows Vista中,提供了一个新的创建信号量内核对象的函数CreateSemaphoreEx,该函数成功返回句柄,失败返回NULL。

复制代码
HANDLE CreateSemaphoreEx(
   PSECURITY_ATTRIBUTES psa,     
// 安全属性结构指针
   LONG lInitialCount,            // 初始可用资源数
   LONG lMaximumCount,       // 最大资源数
   PCTSTR pszName,           // 信号量内核对象的名字(NULL表示匿名)
   DWORD dwFlags,                // 保留型参数,应设置为0
   DWORD dwDesiredAccess);       // 访问限制(参看MSDN)
复制代码

 

  同样,可以打开一个指定名称的信号量,使用OpenSemaphore函数:

HANDLE OpenSemaphore(
   DWORD dwDesiredAccess,     
// 访问限制(参看MSDN)
   BOOL bInheritHandle,        // 是否允许返回的句柄子进程被继承
   PCTSTR pszName);            // 指定的信号量名称

 

  假如,作为一个服务器,有一个缓冲区需要用来存放客户的连接请求,还有一个线程池用来处理连接。但是该缓冲区和线程池的大小有限,比如至多只能同时接纳和处理10位客户的连接请求,而当有10位客户请求连接而尚未处理完成的时候,此时一个新客户也试图建立连接,那么这个连接过程应该被推后,直到有一个连接处理完成之后,这个新客户的连接才能被处理。

  这个时候,可以使用信号量机制来处理线程同步的问题。

  当服务器初始化的时候,最大资源数为10,没有任何服务器请求连接,可以使用如下代码创建信号量内核对象:

HANDLE hSem  =  CreateSemaphore(NULL,  0 10 , NULL);

 

  该函数创建了一个信号量内核对象,最大资源数为10,当前可用资源数为0。由于当前可用资源数为0,所以调用WaitForSingleObject等这些等待函数来等待该信号量句柄的线程都会进入等待状态。

  这些等待函数在内部会查看信号量内核对象的可用资源数,如果该值大于0,则将其减1,线程保持可调度状态,这些比较和设置可用资源数是以原子过程进行的,所以是线程安全的。

  如果可用资源数等于0,则线程进入等待状态,当一个线程将信号量的可用资源数递增之后,某个或某些等待的线程就可以进入就绪状态。

  可以调用ReleaseSemaphore函数来让信号量内核对象的可用资源数递增:

 

BOOL ReleaseSemaphore(
   HANDLE hSemaphore,     
// 信号量内核对象句柄
   LONG lReleaseCount,        // 可用资源增加个数
   PLONG plPreviousCount);     // 返回上次可用资源的数量,一般传递NULL忽略之

 

  可惜的是,Windows没有提供一种方法让我们仅仅是查询当前信号量的可用资源数。

 

  自己总结了一下信号量使用的模型:

复制代码
HANDLE g_hSem;      // 信号量句柄,在其他线程(比如主线程)中创建
DWORD WINAPI ThreadProc(PVOID pvParam)      // 线程函数
{
     
// 等待信号量,如果可用资源大于0,递减资源,线程继续运行,否则线程等待
     WaitForSingleObject(g_hSem, INFINITE);

     
// 访问资源

     
// 访问完毕,释放,递增可用资源数1个(可以根据需要递增n个)
     ReleaseSemaphore(g_hSem,  1 , NULL);
}
复制代码

《Windows via C/C++》学习笔记 —— 内核对象的“线程同步”之“互斥内核对象”

互斥内核对象确保一个线程独占地访问资源。

  互斥内核对象的行为特征和关键代码段有点类似,但是它是属于内核对象,而关键代码段是用户模式对象,这导致了互斥内核对象的运行速度比关键代码段要低。所以,在考虑线程同步问题的时候,首先考虑用户模式的对象。

  但是,互斥内核对象可以跨进程使用,当需要实现多进程之间的线程同步,就可用考虑使用互斥内核对象。而这点,关键代码段无能为力。

 

  在互斥内核对象内部,有以下一些重要的数据:

1、使用计数:表明该互斥内核对象被打开的次数。

2、线程ID

3、递归计数器

  线程ID表明了该互斥内核对象被哪个线程所拥有,递归计数器表明了这个线程(拥有互斥对象)拥有这个互斥对象的次数。

 

  互斥对象的使用规则如下

  • 如果内部线程ID为0(或者是一个无效的线程ID),该互斥内核对象不被任何线程所拥有,会发出通知信号,即处于“已通知”状态。
  • 如果线程ID不为0,而是一个有效的线程ID,那么该互斥内核对象就被这个线程所拥有,而且该互斥内核对象为“未通知”状态。
  • 与其他内核对象不同的是,互斥内核对象在操作系统中有着特殊的代码,允许以不正常的规则进行使用。

  要使用互斥内核对象,首先必须创建它:

HANDLE CreateMutex(
   PSECURITY_ATTRIBUTES psa,  
// 安全属性
   BOOL bInitialOwner,         // 互斥对象是否开始就被调用该函数的线程所拥有
   PCTSTR pszName);            // 该互斥内核对象的名字

 

  Windows Vista中还提供了一个函数用于创建一个互斥内核对象:

HANDLE CreateMutexEx(
   PSECURITY_ATTRIBUTES psa,     
// 安全属性
   PCTSTR pszName,        // 该互斥内核对象的名字
   DWORD dwFlags,         // 互斥对象是否开始就被调用该函数的线程所拥有
   DWORD dwDesiredAccess);        // 访问限制


  第1个函数中的bInitialOwner参数如果为TRUE,则创建的互斥内核对象一开始就被调用这个函数的线程所拥有,它的线程ID被设置为该线程的ID,递归计数器被设置为1。

  如果传递FALSE给这个参数,则互斥内核对象的线程ID和递归计数器被设置为0,表明该互斥内核对象不被任何线程所拥有,该互斥内核对象处于“已通知”状态。

  第2个函数的dwFlags的意义和第1个函数的bInitialOwner参数其实是一样的,0就好比FALSE,CREATE_MUTEX_INITIAL_OWNER就相当于TURE。

  这两个函数成功,返回互斥内核对象的句柄,失败返回NULL。

 

  你通过“名字”来可以打开一个已经创建了的互斥内核对象:

HANDLE OpenMutex(
   DWORD dwDesiredAccess,     
// 访问限制
   BOOL bInheritHandle,        // 是否允许返回的句柄被子进程继承
   PCTSTR pszName);            // 名字

 

  创建了一个互斥内核对象,得到了它的句柄之后,就可以让它保护资源了。

  一个线程中(下面用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。

  也就是说,一个互斥内核对象的递归计数器要大于1,就要让线程多次等待相同的互斥内核对象。

 

  一旦当前线程成功地等待到了一个互斥内核对象之后,该线程就可以独占某些资源,从而可以访问这些共享的资源了。试图访问这些资源的其他线程通过等待相同的互斥对象,就会进入等待状态之中。

  当前线程如果对资源访问结束,必须释放互斥内核对象,使用ReleaseMutex函数:

BOOL ReleaseMutex(HANDLE hMutex);      // 参数是互斥内核对象句柄

 

  该函数将互斥内核对象的递归计数减1。如果一个线程多次成功地等待一个互斥内核对象,就要同样以相同的次数调用ReleaseMutex函数,从而递减其递归计数,当互斥内核对象的递归计数减为0后,其线程ID被设置为0,进入“已通知”状态。
  当这个互斥内核对象进入“已通知”状态之时,系统查看当前是否有线程等待它,如果有,就以公平的原则选择其中一个线程,将这个互斥内核对象的线程ID设置为这个选中的线程的线程ID,互斥对象的递归计数被设置为1。

 

  综合上面所叙述的,可以总结出,互斥内核对象不同于其他内核对象,就是它有一个“线程所有权”的概念,这就使得互斥内核对象比较特殊。

  一个线程调用ReleaseMutex函数释放一个互斥对象,这时系统查看互斥对象的线程ID和这个线程的线程ID是否相同,如果相同,互斥对象的递归计数减1;否则ReleaseMutex不做任何工作,返回FALSE。

  还有一种现象,称做“互斥对象被抛弃”。

  假设一个互斥内核对象为一个线程所拥有,而这个线程却因为某些特殊的原因在终止,比如调用了ExitThread或TerminateThread函数,但是它在终止之前没有释放这个互斥对象。这个时候,系统能够跟踪拥有互斥内核对象的线程内核对象,系统知道这个互斥对象被一个线程抛弃了,就将互斥对象的线程ID设置为0, 将其递归计数设置为0。然后,系统查看是否有其他线程在等待这个互斥对象,如果有,就公平地选中一个,将互斥对象的线程ID设置为选中的线程的线程ID,这和前面的论述是一样的,差别是等待函数返回的值是WAIT_ABANDONED,而不是WAIT_OBJECT_0。这个时候,访问资源是不合适的,因为不知道资源处于何种状态。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值