Windows核心编程 用内核对象进行线程同步

用户模式的线程同步机制:
优点:速度快 ,可保持在用户模式,无需切换到内核模式。
缺点: 不适用于许多应用程序 不支持多个进程间的同步
内核对象的 唯一缺点就是性能:X86上,一个空的系统调用会占约200个CPU周期(原因是用户模式切换到内核模式时,伴随调度新线程而来的刷新高速缓存以及错过高速缓存(未命中))。
        几乎所有的内核对象都可以进行线程同步。这些内核对象都包括两种状态:一种是触发状态,另一种是未触发状态。例如,进程和线程对象在刚创建的时候是未触发状态,当进程和线程终止时就变成了触发状态。此状态转换是不可逆的。之所以能进行从未触发到触发的转变是因为内核对象内部有一个布尔变量。系统创建内核对象时会把它的初始值设为false,表示未触发 。内核对象状态的切换就是对此值进行切换。
一些内核对象:
进程
线程
作业
文件以及控制台的标准输入流/输出流/错误流
事件
可等待的计时器(waitable timer)
信号量
互斥量
9.1 等待函数:
等待函数 使一个线程进入等待状态,直到它所等待的内核对象被触发为止。
WaitForSingleObject系列等待函数。内部为原子操作
DWORD  WaitForSingleObject
(
HANDLE  hHandle,         // handle to object
DWORD  dwMilliseconds    // time-out interval毫秒,一般传入INFINITE(OXFFFFFFFF -1)
);
返回值:
Value Meaning
WAIT_FAILED 传入无效参数,可调用GetLastError查看
WAIT_OBJECT_0 等待的对象被触发
WAIT_TIMEOUT 时间耗尽
DWORD  WaitForMultipleObjects(//对多个对象操作
   DWORD  nCount,              // number of handles in array 最大为 MAXIMUM_WAIT_OBJECTS
   CONST  HANDLE  *lpHandles,   // object-handle array
   BOOL  bWaitAll,             // wait option true所有都触发 false 一个触发就返回
   DWORD  dwMilliseconds       // time-out interval);
bWaitAll为true时,返回值同上,为false时,当对象被触发时,返回WAIT_OBJECT_0+dwCount-1间的任何一个值。
例:
HANDLE  h[3];  
h[0]=hProcess1;  
h[1=hProcess2;  
h[2]=hProcess3;  
DWORD  dw=WaitForMultipleOBjecs(3,h, false ,5000);  
switch (dw)  
{  
    case  WAIT_OBJEC_0: //第一个对象被触发。  
        break ;  
  case  WAIT_OBJEC_0+1: //第二个对象被触发。  
        break ;  
  case  WAIT_OBJEC_0+2: //第三个对象被触发。  
        break ;  
  case  WAIT_TIMEOUT: //超时  
        break ;  
  case  WAIT_FAILED: //句柄无效。  
        break ;  
}
9.2 等待成功所引起的副作用:可能会改变对象的状态。
9.3 事件内核对象
事件包含使用计数,重置方式(自动,手动) 触发标志( 触发 状态 还是 未 触发 状态
所谓自动重置事件,就是当线程等待自动重置事件成功后,对象将自动重置成非触发状态。  

使用 CreateEvent 创建一个事件内核对象。
HANDLE  CreateEvent(//还有个CreateEventEX函数
   LPSECURITY_ATTRIBUTES lpEventAttributes,  // SD
   BOOL  bManualReset,                        // reset type true为手动重置 false自动重置 
   BOOL  bInitialState,                       // initial state true 初始化为触发状态 false 初始化为未触发
   LPCTSTR  lpName                            // object name
);
跨进程同步时,可以调用CreateEvent并在pszName中传入相同的值;使用继承;使用DuplicateHandle函数;调用OpenEvent并在pszName参数中指定与CreateEvent中相同的名字。
BOOL   SetEvent(   HANDLE   hEvent    // handle to event); 把事件变为触发状态
BOOL ResetEvent(  HANDLE hEvent   // handle to event);把事件变为非触发状态
例:
HANDLE  g_hEvent;
int  WINAPI _tWinMain(...)
{
g_hEvent=CreateEvent(NULL,TRUE,FALSE,NULL); //创建一个手动重置的对象并将这个事件对象放在一个全局变量中
 
DWORD  dwThreadID;
 
HANDLE  hThrasd[3];
 
hThrasd[0]=_beginthreadex(NULL,0,WordCount,NULL,0,&dwThreadID);
 
hThrasd[1]=_beginthreadex(NULL,0,SpellCheck,NULL,0,&dwThreadID);
 
hThrasd[1]=_beginthreadex(NULL,0,Gramer,NULL,0,&dwThreadID);
 
OpeFileAndReadIntoMemory();
SetEvent(g_hEvent);
...
}
DWORD  WINAPI WordCount( PVOID  pvParam)
 
{
 
WaitForSingleObject(g_hEvent,INFINITE);
 
return  0;
 
}
 
DWORD  WINAPI SpellCheck( PVOID  pvParam)
 
{
 
WaitForSingleObject(g_hEvent,INFINITE);
 
return  0;
 
}
 
DWORD  WINAPI Gramer( PVOID  pvParam)
 
{
 
WaitForSingleObject(g_hEvent,INFINITE);
 
return  0;
 
}
9.4 可等待的计时器内核对象 
可等待的计时器是这样一种内核对象,它们会在某个指定的事件触发,或没隔一段时间触发一次。它们通常用来在某个时间执行一些操作。
HANDLE  CreateWaitableTimer(  LPSECURITY_ATTRIBUTES lpTimerAttributes,  // SD
   BOOL  bManualReset,                        // reset type 同上,true手动重置;false 自动重置
   LPCTSTR  lpTimerName                       // object name);
对应的OpenWaitableTimer得到一个已经存在的可等待计时器的句柄。
当创建时,其总是处于未触发状态。
BOOL  SetWaitableTimer(//用来触发
   HANDLE  hTimer,                           // handle to timer
   const  LARGE_INTEGER *pDueTime,           // timer due time 什么时间触发,为负值时是相对时间
   LONG  lPeriod,                            // timer interval 多久触发一次 都是以毫秒为单位,为0时表示只触发一次
   PTIMERAPCROUTINE pfnCompletionRoutine,   // completion routine
   LPVOID  lpArgToCompletionRoutine,         // completion routine parameter
   BOOL  fResume                             // resume state);
例一:
HANDLE  hTimer;  
SYSTEMTIME St;  //表示本地时间
FILETIME ftLocal ,ftUTC;  
LARGE_INTEGER liUTC;  
hTimer=CreateWaitableTimer(NULL, false ,NULL);  
St.wyear=2013;  //年
st.wMonth=1;  //月
st.wDayOfWeek=0;  //忽略
st.wDay=1;  //日
st.wHour=1;  
st.wMinute=1;  
st.wSecond=0;  
st.wMillisecons=0;//精确到毫秒  
SystemTimeToFileTime(&st,&ftLocal); //SYSTEMTIME结构转换为FILETIME结构。  
LocalFileTimeToFileTime(&ftLocal,&ftUTC);  //将本地时间转换为UTC(全球标准时间) time
liUTC.LowPart=ftUTC.dwlowDateTime;  
liUTC.HighPart=ftUTC.dwHighDateTime;  
SetWaitableTimer(hTimer,&liUTC,6*60*60*1000,NULL,NULL, false );  
注意:FILETIME和LARGE_INTEGER的二进制结构完全相同,但是对齐方式不同,前者是32位,后者是64位
例二:
HANDLE  hTimer;  
LARGE_INTEGER li;  
hTimer=CreateWaitableTimer(NULL, false ,NULL);  
Li.quadpart=-(5*10000000);  //设置为相对时间,第一次触发事假为调用结束的5秒后
SetWaitableTimer(hTimer,&li,6*60*60*1000,NULL,NULL, false );  
bResume用以支持挂起。一般都传入false。当传入true时,当计时器被触发时,系统就会使机器结束挂起模式,并唤醒等待该计时器的线程。当为false时,计时器会被触发,但是如果此时机器处于挂起态时,在机器继续执行之前,被唤醒的任何线程都得不到cpu。
当通过SetWaitTimer函数设置了一个等待定时器的属性之后,你可以通过CancelWaitableTimer函数来取消这些设置:
BOOL CancelWaitableTimer(HANDLE hTimer);

9.4.1 让可等待的计时器添加APC(异步过程调用)调用【暂时不理解】
9.4.2 计时器的剩余问题
1.创建多个计时器内核对象会影响性能,可以利用CreateThreadpoolTimer函数改善。
2.   可等待计时器和用户计时器(通过SetTimer函数来设置)的最大区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。此外可等待计时器是内核对象,这意味着它们不仅可以在多个线程间共享,而且可以具备安全性。此外,用户计时器会产生WM_TIMER消息,这个消息被送到SetTimer设置的回调函数。此时只有一个线程得到通知。而可等待计时器对象可以被多个线程等待。如果打算在计时器被触发时执行与用户界面相关的操作。使用用户计时器可使代码更容易编写。
9.5 信号量内核对象
信号量内核对象用来对资源进行计数( 原子操作)。与其他内核对象相同,它包括一个使用计数。但是它还包含另外两个值:一个是最大资源计数和当前资源计数。最大资源计数表示信号量可以控制的最大资源数量。当前资源计数表示信号量当前可用资源的数量。
信号量的规则如下:
1:如果当前资源计数大于0,那么信号量处于触发状态(说明有资源可被使用,所有等待线程被调度)。
2:如果当前资源计数等于0,那么信号量处于未触发状态(没有可用资源,所有线程等待)。
3:当前资源计数不会小于0.
4:当前可用资源计数不会大于最大资源计数。
注意:使用信号量时不要将信号量的使用计数和当前资源计数混为一谈。
HANDLE  CreateSemaphore(
    PSECURITY_ATTRIBUTE psa,      //安全属性结构指针
    LONG  lInitialCount,           //初始可用资源数
    LONG  lMaximumCount,           //最大资源数
    PCTSTR  pszName);              //信号量内核对象的名字
同样,可以打开一个指定名称的信号量,使用OpenSemaphore函数:
HANDLE OpenSemaphore(  DWORD dwDesiredAccess/*一般传入SEMAPHORE_ALL_ACCESS*/, BOOL bInheritHandle,  PCTSTR pszName);  

例:HANDLE hSem = CreateSemaphore(NULL, 0, 5, NULL);
BOOL   ReleaseSemaphore(// 递增信号量的当前资源计数 等待函数会将资源数减一
 
   HANDLE  hSemaphore,//信号量的句柄
 
   LONG  lReleaseCount,  //增加个数,必须大于0且不超过最大资源数量
 
   LPLONG  lpPreviousCount //用来传出先前的资源计数,设为NULL表示不需要传出
 
);
例子( 用于线程同步,信号量的用途很多):
#include <stdio.h>  
#include <process.h>  
#include <windows.h>  
long  g_nNum;  
unsigned  int  __stdcall Fun( void  *pPM);  
const  int  THREAD_NUM = 10;  
//信号量与关键段  
HANDLE             g_hThreadParameter;  
CRITICAL_SECTION  g_csThreadCode;  
int  main()  
{  
     printf ( "     经典线程同步 信号量Semaphore\n" );  
     printf ( " -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n" );  
   
     //初始化信号量和关键段  
     g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL); //当前0个资源,最大允许1个同时访问  
     InitializeCriticalSection(&g_csThreadCode);  
   
     HANDLE   handle[THREAD_NUM];   
     g_nNum = 0;  
     int  i = 0;  
     while  (i < THREAD_NUM)   
     {  
         handle[i] = ( HANDLE )_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
         WaitForSingleObject(g_hThreadParameter, INFINITE); //等待信号量>0  
         ++i;  
     }  
     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
       
     //删除信号量和关键段  
     DeleteCriticalSection(&g_csThreadCode);  
     CloseHandle(g_hThreadParameter);  
     for  (i = 0; i < THREAD_NUM; i++)  
         CloseHandle(handle[i]);  
     return  0;  
}  
unsigned  int  __stdcall Fun( void  *pPM)  
{  
     int  nThreadNum = *( int  *)pPM;  
     ReleaseSemaphore(g_hThreadParameter, 1, NULL); //信号量++  
   
     Sleep(50); //some work should to do  
   
     EnterCriticalSection(&g_csThreadCode);  
     ++g_nNum;  
     Sleep(0); //some work should to do  
     printf ( "ID = %d  NUMS = %d\n" , nThreadNum, g_nNum);  
     LeaveCriticalSection(&g_csThreadCode);  
     return  0;  
}结果:
 
9.6 互斥量内核对象
互斥量内核对象用来确保一个线程独占第一个资源的访问( 原子操作)。互斥量对象包括一个使用计数、线程ID(标识占用这个互斥量的线程)以及一个递归计数(这个线程占用该互斥量的次数)。

互斥内核对象的行为特征和关键代码段有点类似,但是它是属于内核对象,而关键代码段是用户模式对象,这导致了互斥内核对象的运行速度比关键代码段要低。

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

互斥量的规则:
1、如果线程ID为0(无效线程ID),那么该互斥量不为任何线程所占用,它处于触发状态;
2、如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发状态;
3、与所有其它内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则;
创建一个互斥量:
HANDLE   CreateMutex(
PSECURITY_ATTRIBUTES psa, 
BOOL  bInitialOwner, //FALSE:ID和递归计数=0;TRUE:ID=调用线程ID,递归计数=1
PCTSTR  pszName); 
OpenMutex来打开一个已存在的互斥量。
HANDLE  OpenMutex(  
    DWORD  dwDesiredAccess,  
    BOOL  bInheritHandle,  
    PCTSTR  pszName); 
为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程ID,递归计数为1。
否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。
当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态 。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。 如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。 因此,使递归对象大于1 的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。这是与其他内核对象不一样的地方
BOOL  ReleaseMutex( HANDLE  hMutex);  
9.6.1 遗弃问题
互斥量具有线程所有权的问题。这就可能导致一些问题: 如果占用互斥量的线程在释放互斥量之前终止,对于互斥量来说会发生什么情况呢?答案是: 互斥量被遗弃
系统会检测到互斥量被遗弃的情况,会自动的将互斥量对象的线程ID置为0,然后再检查有没有其他线程等待该互斥量。如果有线程等待,系统会从等待队列中选取一个,将其变为可调度状态。这一切都和原来没什么不同,唯一的区别是等待函数不再返回WAIT_OBJEC_0,而是返回 WAIT_ABANDONED。这个特殊的返回值只适用于互斥量。
9.6.2 互斥量和关键段的比较

特征

互斥量

关键段

性能

是否能跨进程使用

声明

HANDLE hmtx;

CRITICAL_SECTION cs;

初始化

hmtx = CreateMutex (NULL, FALSE, NULL);

InitializeCriticalSection(&cs);

清理

CloseHandle(hmtx);

DeleteCriticalSection(&cs);

无限等待

WaitForSingleObject (hmtx, INFINITE);

EnterCriticalSection(&cs);

0等待

WaitForSingleObject (hmtx, 0);

TryEnterCriticalSection(&cs);

任意时间长度的等待

WaitForSingleObject (hmtx, dwMilliseconds);

不支持

释放

ReleaseMutex(hmtx);

LeaveCriticalSection(&cs);

是否能同时等待其它

内核对象

是 (使用WaitForMultipleObjects 或类似函数)

9.7 线程同步对象速查表

对象

何时处于未触发状态

何时处于触发状态

成功等待的副作用

进程

当进程仍在运行的时候

当进程终止运行时(ExitProcess,

Te rminateProcess)

线程

当线程仍在运行时

当线程终止运行时(ExitThread,

TerminateThread)

作业

当作业尚未超时的时候

当作业超时的时候

文件

当I / O请求正在处理时

当I / O请求处理完毕时

控制台输入

不存在任何输入

当存在输入时

文件修改通知

没有任何文件被修改

当文件系统发现修改时

重置通知

自动重置事件

ResetEvent , PulseEvent或等待成功

当调用SetEvent / PulseEvent时

重置事件

手动重置事件

ResetEvent或PulseEvent

当调用SetEvent / PulseEvent时

自动重置等待计时器

CancelWaitableTimer或等待成功

当时间到时(SetWaitableTimer)

重置定时器

手动重置等待计时器

CancelWaitableTimer

当时间到时(SetWaitableTimer)

信号量

等待成功

当数量> 0时(ReleaseSemaphore)

数量递减1

互斥对象

等待成功

当未被线程拥有时(Release互斥对象)

将所有权赋予线程

关键代码段(用户模式)

等待成功((Try)EnterCriticalSection)

当未被线程拥有时(LeaveCriticalSection)

将所有权赋予线程

SRWLock
(用户模式)
等待成功的时候
(AcquireSRWLock(Exclusive))
不为线程占用的时候
(ReleaseSRWLock(Exclusive))
把所有权交给线程
条件变量
(用户模式)
等待成功地时候
(SleepConditionVariable*)
被唤醒的时候
(Wake(All)ConditionVariable)
没有
InterLocked系列函数(用户模式)从来不会使线程变成不可调度状态,它们只是修改一个值并返回。
9.8 其他的线程同步函数(暂时跳过)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值