内核的同步方式

来源:互联网 发布:java开源微信商城 编辑:程序博客网 时间:2024/05/02 16:05

1. 第八章所有的同步函数都是工作在用户模式下,而如果要使用内核模式下的线程同步函数需要耗费一定的时间作为代价.

线内核对象的两种状态:

● 触发:对象调度结束

● 未触发:对象正在调度.

其实,进行内核对象的内部有一个布尔变量,当系统创建内核对象的时候会把这个变量的值初始化为FALSE(为触发).当进程终止的时候,操作系统会自动把相应的内核对象中的这个值设为TRUE.表示该对象已经触发.

2. 等待函数使一个线程自愿进入等待状态,直到指定的内核对象被触发为止.需要注意的是,如果线程在调用一个等待函数的时候,相应的内核对象已经处于触发状态,那么线程是不会进入等待状态的.

DWORD WaitForSignalObject(

HANDLE hObject, //标示要等待的内核对象,这个内核对象可以处于触发或未触发状态

DWORD dwMilliseconds);//指定线程最多愿意花多长时间来等待对象被触发.

//如果取值为INFINITE表示线程希望一直等待.知道hObject表示的线程终止为止

函数的返回值:如果等待的对象被触发(成功),那么返回WAIT_OBJECT_0;如果等待超时,那么返回WAIT_TIMEOUT.如果给WaitForSignalObject传入了无效的参数,那么返回值是WAIT_FAILED(可以调用GetLastError来获取详细信息).

如果想要检测多个内核对象是否已触发,可使用WaitForMultipleObject:

DWORD WaitForMultipleObject(

DWORD dwCount,//检测内核对象的数量(取值范围1~64)

CONST HANDLE* phobjects,//指向一个内核对象句柄的数组

BOOL bWaitAll,//等待单个或所有内核对象满足条件(TRUE等待所有内核对象返 //回)

DWORD dwMilliseconds);//同上.

返回值说明:如果bWaitAll为TRUE且所有的对象都被已触发,那么返回WAIT_OBJECT_0,如果为FALSE,那么只要任何一个对象被触发,函数就会立即返回.(返回值范围:WAIT_OBJECT_0到WAIT_OBJECT_0 + dwCount -1之间的值.

3. 等待成功所引起的副作用:当等待的内核对象全部处于触发状态,在函数返回后会使得它重新变为非触发状态.进程和线程内核对象完全没有副作用.WaitForMultipleObject是以原子方式工作的.(这样可以防止发生死锁).

如果多个线程等待同一个内核对象,那么将有系统决定哪个线程最先被唤醒.(其实他的原理就和"先入先出"的机制类似.靠等待时间来唤醒线程).但是如果线程被挂起以后,调度将会被打乱.

4. 事件内核对象:包含一个使用计数、用来表示事件是自动重置事件还是手动重置事件的布尔值),以及另一个用来表示事件有没有被触发的布尔值.

手动重置事件:当一个手动重置事件被触发的时候,正在等待该事件的所有线程都将变成可调度状态.

自动重置事件:只有一个正在等待该事件的线程会变成可调度状态.

事件通常的用途是,让一个线程执行初始化工作,然后再触发另一个线程,让他执行剩余的工作.一开始我们将事件初始化为未触发状态,然后当线程完成初始化的工作时,触发事件,这时另外一个线程一直在等待该事件,它发现事件被触发,于是变成可调度状态.

创建:

HANDLE CreateEvent(

PSECURITY_ATTRIBUTES psa,

BOOL bManualReset,//手动重置事件还是自动重置事件

BOOL bInitialState,//初始化为触发状态还是为触发状态

PCTSTR pszName);

Vista提供的最新函数:

HANDLE CreateEventEx(

PSECURITY_ATTRIBUTES psa,

PCTSTR pszName,

DWORD dwFlags,

DWORD dwDesiredAccess);

其中dwFlags的取值:

WinBase.h中定义的常量

描述

CREATE_EVENT_INITIAL_SET

(0x02)

等价于CrateEvent中传入的bInitialState参数.如果设置了这个标志,那么函数会将事件初始化为触发状态,否则初始化为未触发状态

CREATE_EVENT_MANUAL_RESET

(0x01)

等价于CreateEvent中传入bManualReset参数.如果设置了这个表示,那么创建的是一个手动重置事件,否则创建的是一个自动重置事件.

dwDesiredAccess:允许我们制定在创建事件时返回的句柄对事件有何种访问权限.

一旦创建了事件,我们可以使用SetEvent来直接控制事件的状态.

BOOL SetEvent(HANDLE hEvent);

使用ResetEvent把事件变成未触发状态.

BOOL ResetEvent(HANDLE hEvent).

Microsoft为自动重置事件定义了一个等待成功所引起的副作用:当线程成功等到自动重置事件对象的时候,对象会自动地重置为为触发状态.(也就是说,如果我们想要使用事件来达到同步的话,不该产生自动重置事件,而应该产生手动重置事件).

BOOL PulseEvent(HANDLE hEvent);//会触发事件然后立刻将其恢复到未触发状态.相当于连续调用了SetEvent和ResetEvent函数.

理解:自动重置事件可以想象成就是为了让两个线程互斥访问某个资源.当一个线程启动时,系统再把事件内核对象重置为未触发状态使得其他的线程只能等待当前线程释放.

几个函数讲解:

5. 可等待的计时器内核对象:类似于定时器,在某个指定的时间触发,或每隔一段时间触发一次.通常用来在某个时间执行一些操作.

创建可等待的计时器:

HANDLE CreateWaitableTimer(

PSECURITY_ATTRIBUTES psa,//安全访问属性

BOOL bManualReset,//创建一个手动重置事件还是自动重置时间计时器

PCTSTR pszName);//命名

可以使用OpenWaitanleTimer函数来得到一个已经存在的可等待计时器的句柄.该句柄与当前进程相关联:

HANDLE OpenWaitableTimer(

DWORD dwDesiredAccess,

BOOL  bInheritHandle,

PCTSTR pszName);

当可等待的计时器对象总是处于未触发状态.如果想要触发计时器时,需调用一下函数:

BOOL SetWaitableTimer(

HANDLE hTimer,//句柄

Const LARGE_INTEGER* pDueTime,//计时器第一次触发的时间应该是什么时候

LONG lPeriod,//在第一次触发之后,触发的频率应该是多少

PTIMERAPCROUTINE pfnCompletionRoutine,

PVOID pvArgToCompletionRoutine,

BOOL bResume);//是否挂起,为TRUE表示机器结束挂起模式

◆ 关于时间的一些知识讲解:具体见我的另外两篇转载的博客:

地址1:http://blog.csdn.net/yuanweihuayan/article/details/6853911

地址2:http://blog.csdn.net/yuanweihuayan/article/details/6853914

几者的转换为:

SYSTEMTIME->FILETIME->LARGE_INTEGER

SystemTimeToFileTime(SYSTEMTIME,FILETIME)->LocalFileTimeToFileTime(FILETIME,FILETIME)->然后将其连个为分别赋给LowPart和HighPart两个位即可.

 typedef struct _FILETIME {  

DWORD dwLowDateTime;  

                          DWORD dwHighDateTime;  

 } FILETIME, *PFILETIME;  

 typedef struct _SYSTEMTIME {  

                           WORD wYear;  

                            WORD wMonth;  

                             WORD wDayOfWeek;  

                             WORD wDay;  

                             WORD wHour;  

                             WORD wMinute;  

                             WORD wSecond;  

                             WORD wMilliseconds;  

 } SYSTEMTIME, *PSYSTEMTIME;  

typedef union _LARGE_INTEGER { 

  struct {

      DWORD LowPart; 

      LONG  HighPart; 

  };

  LONGLONG QuadPart;

} LARGE_INTEGER, *PLARGE_INTEGER;

我们可以给计时器传递一个相对时间.(向pDueTime传一个负值.必须是100纳秒的整数倍)

比如我想要设置计时器在调用函数后的4秒钟后可以给它传递-(4 * 10 000 000)即可

我们也可以设计一个一次性计时器:即向lPeriod传递0即可.

取消计数器:

BOOL CancelWaitableTimer(HANLE hTimer);

如果希望时间一到,把一个异步过程调用(APC)添加到队列中去,可以实现一个APC函数,并传给pfnCompletionRoutine.

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,

DWORD dwTimerLowValue,DWORD dwTimerHighValue){}

计时器被触发时,当且仅当SetWaitableTimer的调用线程正处于可提醒状态时,这个函数会被同一个线程调用.(也就是我们在等待计时器时调用SleepEx,WaitForSignalObjectEx、WaitForMultipleObjectEx、MsgWaitForMultipleObjectEx或者SignalObjectAndWait而进入等待状态).如果线程并非在其中一个函数内等待,那么系统不会把计时器的APC函数添加到队列中.(避免资源浪费).

一般使用步骤如下:

void SomeFun()

{

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

LARGE_INTEGER li = {0};

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

SleepEx( INFINITE,TRUE );

CloseHandle( hTimer );

}

用户计时器与可等待计时器的区别:

最大的区别是,用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源,此外可等待计时器时内核对象,意味着它们不仅可以再多个线程间共享,而且可以具备安全性.用户计时器只能是一个线程得到通知,而等待计时器如果是手动的,那么将有多个线程得到通知.

6. 信号量内核对象用来对资源进行计数.组成:

■ 使用计数

■ 另外两个32位值:一个最大资源计数和一个当前资源计数.最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量.

信号量的规则:

■ 如果当前资源计数大于0,那么信号量处于触发状态

■ 如果当前资源计数等于0,那么信号量处于未触发状态

■ 系统绝对不会让当前资源计数变为负数

■ 当前资源计数绝对不会等于最大资源计数

创建信号量:

HANDLE CreateSemaphore(

PSECURITY_ATTRIBUTES psa,

LONG lInitialCount,//指向开始有多少个可供使用的资源

LONG lMaximumCount,//指向应用程序能够处理的资源的最大数量

PCTSTR pszName);

或者:

HANDLE CreateSemaphore(

PSECUTRY_ATTRIBUTE psa,

LONG lInitialCount,

LONG lMaximumCount,

PCTSTR pszName,

DWORD dwFlags,//系统保留(为0)

DWORD dwDesiredAccess);//访问权限

获取句柄

HANDLE OpenSemaphore(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

递增信号量的当前资源计数:

BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lReleaseCount,//增加的资源数

PLONG plPreviousCount);//当前计数的原始值\

另外不能使用ReleaseSemaphore传给lReleaseSemaphore0或者很大的值来获取当前资源计数

7 互斥量内核对象用来确保一个线程独占对一个资源的访问.

■ 组成:

①使用计数

②线程ID:标示当前占用这个互斥量的是系统中那个线程

③递归计数: 表示这个线程占用该互斥量的次数.

■ 与关键代码段的区别:

● 互斥量是内核模式下的,而关键代码段是用户模式下的.也意味着关键代码段比 互斥量要快.

● 不同进程中的线程可以访问同一个互斥量.

■ 互斥量的规则:

● 如果线程ID为0(即无效线程ID),那么该互斥量不为任何线程锁占有,它处于触 发状态

● 如果线程ID为非0,那么有一个线程已经占用了该互斥量,它处于未触发状态

● 与所有其他内核对象不同,OS对互斥量进行了特殊处理,欲行它们违反一些常 规的规则.

■ 互斥量的一些操作

● 创建

HANDLE CreateMutex(

PSECUTRY_ATTRIBUTES psa,

BOOL bInitialOwner,//互斥量的初始状态(TRUE表示处于未触发状态)线程ID //和递归计数分别设为当前线程ID和1

PCTSTR pszName);

HANDLE CreateMutexEx(

PSECUTRY_ATTRIBUTES psa,

PCTSTR pszName,

DWORD dwFlags,//相当于bInitialOwner(0表示 //FALSE,CREATE_MUTEX_INITIAL_OWNER等价于TRUE

DWORD dwDesiredAccess);

● 获取互斥量句柄

HANDLE OpenMutex(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

● 释放互斥量,使其处理可触发状态.

BOOL ReleaseMutex(HANDLE hMutex);

函数工作方式:使得对象的递归计数减一.如果线程成功地等待了互斥量对象不止一次,那么线程必须调用相同次数ReleaseMutex使得递归计数为0,并且此时线程ID也设为0.这样就出发了对象.

互斥量与其他内核对象的区别:

加入线程试图等待一个未触发的互斥量对象,线程就会进入等待状态,但是如果系统检查想要获得互斥量的线程的ID与互斥量对象内部记录的线程ID相等那么此时系统也会让线程保持可调度状态--即使该互斥量尚未触发.

■ 互斥量与其它内核对象不同的根本原因是"所有权"问题:也就是说,其它内核对象不会记住自己是哪个线程等待成功的.这就使得他即使在未触发状态下也能为线程所得.

这样就会出现一种情况是,如果占用互斥量的线程在释放互斥量之前终止,那么互斥量将出现"遗弃".

由于系统记录着所有的互斥量和线程内核对象,所以他有能力使得被遗弃的互斥量重新变为触发状态.这样其他的等待互斥量的线程就可以获取到互斥量的资源.但是这里有一个小小的不同:处于等待的函数不在返回通常的WAIT_OBJECT_0而是返回WAIT_ABANDONED.

■ 互斥量和关键段的区别:

特征

互斥量

关键段

是否能跨进程使用

声明

HANDLE hmtx

CRITICAL_SECTION cs

初始化

Hmtex = CreateMutex(NULL,FALSE,NULL)

InitializeCriticalSection(&cs)

清理

CloseHandle(hmtx)

DeleteCriticalSection(&cs)

无限等待

WaitForSignalObject(hmtx,INFINITE)

EnterCriticalSection(&cs)

0等待

WaitForSignalObject(hmtx,INFINITE)

TryEnterCriticalSection(&cs)

任意时间长度等待

WaitForSingleObject(hmtx,dwMilliseconds)

不支持

释放

ReleaseMutex(hmtx)

LeaveCriticalSection(&cs)

能否同时等待其他内核对象

是(使用WaitForMultipleObject或者类似的函数)

线程同步对象速查表:

对象

何时处于未触发状态

何时处于触发状态

成功等待的副作用

进程

进程仍在运行

进程终止时(ExitProcess或者TerminateProcess)

没有

线程

线程仍在运行时

线程终止的时候(Exit(Terminate)Thread

没有

作业

作业尚未超时时

作业超时时

没有

文件

有待处理的I/O请求的时

I/O请求完成时

没有

控制台输入

没有输入时

有输入时

没有

文件变更通知

文件没有变更通知时

文件系统检测到变更时

重置通知

自动重置事件

ResetEvent,PulseEvent或等待成功时

SetEvent/PulseEvent被调用时

重置事件

手动重置事件

ResetEvent,PulseEvent

SetEvent/PulseEvent被调用时

没有

自动重置可等待计时器

CancelWaitableTimer或者等待成功的时候

时间到时(SetWaitableTimer)

重置计数器

手动重置可等待计时器

CancelWaitableTimer

时间到时(SetWaitableTimer)

没有

信号量

等待成功时

计数大于0(ReleaseSemaphore)

计数器减一

互斥量

等待成功的时候

不为线程占用时(ReleaseMutex)

把所有权交给线程

关键段(用户模式)

等待成功时((Try)EnterCriticalSection)

不为线程占用时(LeaveCriticalSection)

把所有权交给线程

SRWLock(用户模式)

等待成功时(AcquireSRWLock(Exclusive))

不为线程占用时(ReleaseSRWLock(Exclusive))

把所有权交给线程

条件变量(用户模式)

等待成功时(SleepConditionVariable*)

被唤醒时(Wake(All)ConditionVariable)

没有

Interlocked系列函数(用户模式)从来不会使得线程变为不可调度状态,它们只是修改一个值,并立即返回.

8 异步设备I/O:允许线程开始读取操作或者写操作,而不需等待读取操作或者等待操作完成.设备对象时可同步的内核对象,即我们可以调用WaitForSignalObject,并传入文件句柄、套接字、通信端口等等。当系统执行异步的I/O的时候,设备处于为触发状态,一旦操作完成,系统将变成触发状态。这样线程就知道操作已经完成了,线程就可以继续执行。

函数:DWORD WaitForInputIdle(

HANDLE hProcess,

DWORD  dwMillseconds);

先挂起当前进程,然后等待hProcess标示的进程,直到创建应用程序第一个窗口的(也就是hProcess标示的进程)线程中没有待处理的输入为止.

线程也可以调用MsgWaitForMultipleObjects或MsgWaitForMultipleObjsEx,使得线程等待需要自己处理的消息.

DWORD MsgWaitForMultipleObjects(

DWORD dwCount,

PHANDLE phObjects,

BOOL bWaitAll,

DWORD dwMilliseconds,

DWORD dwWakeMask);

DWORD MsgWaitForMultipleObjectsEx(

DWORD dwCount,

PHANDLE phObjects,

DWORD dwMilliseconds,

DWORD dwWakeMask,//标示是哪类消息.

DWORD dwFlags);

这些函数与WaitForMultipleObjects类似,不同之处在于,不仅内核对象被触发的时候调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,他们也会变成可调度状态.

创建窗口的线程和执行与用户界面相关的任务线程都不应该使用WaitForMultipleObjects,而应该使用MsgWaitForMultipleObjectsEx.因为前者会妨碍线程对用户在用户界面上的操作进行响应.

9. 当调试器开始执行的时候,会将自己附着到被调试的程序.然后调试器只是在一边闲着,等待操作系统通知它有与被调试程序相关的时间发生.调试器通过WaitForDebugEvent函数类等待这些时间:

BOOL WaitForDebugEvent(

PDEBUG_EVENT pde,

DWORD  dwMillsecond);

当调试器调用这个函数的时候,调试器的线程会挂起,系统通过让WaitForDebugEvent返回的方式,来通知调试器有调试事情发生.参数pde指向的结构包含了与刚才发生的调试事件有关的信息.

10. SignalObjectAndWait函数通过一个原子操作来触发内核对象并等待另一个内核对象:

DWORD SignalObjectAndWait(

HANDLE hObjectToSignal,//标示的必须是一个互斥量、信号量或者事件.

HANDLE hObjectToWaitOn,//标示互斥量、信号量、事件、计时器、进程、线程、 //作业、控制台输入以及变更通知.

DWORD dwMilliseconds,//最多应该花多长时间来等待对象触发

BOOL bAlertable); //当线程处于等待状态时,是否能够对添加到队列中的异步 //过程调用进行处理

hObjectToSignal:任何其它类型对象将导致函数返回WAIT_FAILED.这时使用GetLastError会返回ERROR_INVALID_HANDLE.该函数内部会检查对象的类型并分别执行与ReleaseMutex、ReleaseSemaphore(使用计数减一)或者SetEvent等价的操作.

返回值:WAIT_OBJECT_0,WAIT_OBJECT_TIMEOUT,WAIT_FAILED,WAIT_ABANDONED或者WAIT_IO_COMPLETION.

函数的优点:花费时间短.

10. Vista提供了一组新的等待链遍历(简称WCT)API,这些函数可以让我们列出所有锁,并检测进程内部,甚至是进程之间的死锁.

可能的锁

描述

关键段

Windows会记录哪个线程正在占用哪个关键段

互斥量

Windows户记录哪个线程正在占用哪个互斥量,即使被遗弃

进程和线程

Window会记录哪个线程正在等待进程终止或者线程终止

SendMessage调用

知道那个线程正在等待SendMessage调用返回时很重要的

COM初始化和调用

Windows会记录对CoCreateInstance的调用以及对COM对象的方法的调用

高级本地过程调用

在windows Vista中,作为新的未公开的内核进程间通信机制,ALPC已经取代了本地过程调用.