现在的位置: 首页 > 综合 > 正文

Windows核心编程【9】小结

2014年09月05日 ⁄ 综合 ⁄ 共 9136字 ⁄ 字号 评论关闭

第九章 用内核对象进行线程同步

1、在用户模式下进行线程同步的最大好处就是速度非常快。如果关心应用程序的兴宁,那么应该先看看用户模式下的同步机制能否适用。

2、但是用户模式下的同步机制存在一些局限性。例如,对Interlocked系列函数只能对一个值进行操作,它们从来不会把线程切换到等待状态。我们可以用关键段来把线程切换到等待状态,但是它们只能用来对同一个进程中的线程进行同步。此外,在使用关键段的时候我们很容易陷入死锁的情形,这是因为我们无法进入关键段指定一个最长等待时间。
3、内核对象唯一的缺点就是它们的性能。本章中的任何一个新函数,调用线程必须从用户模式切换到内核模式,这种切换是非常耗时的,在x86平台上,一个空的系统调用大概会占用200个CPU周期。当然,还不包括执行被调用函数在内核模式下的实现代码。但是,造成内核对象比用户模式下的同步机制慢几个数量级的原因,是伴随着调度新线程而来的刷新高速缓存以及错过高速缓存(未命中)。这里谈论的是成百上千个CPU周期。
4、对线程同步来说,内核对象中的每一种要么处于触发(signaled)状态,要么处于未触发(nonsignaled)状态。MS为每种对象创建了一些规则,规定如何在这两种状态之间进行转换。例如:进程内核对象在创建的时候总是处于未触发状态。当进程终止的时候,操作系统会自动使进程内核对象变为触发状态。当进城内核对象被触发后,它将永远保持这种状态,再也不会变回到未触发状态。
5、在进程内核对象的内部有一个布尔变量,当系统创建内核对象的时候会把这个变量的值初始化为FALSE(未触发)。当进程终止的时候,操作系统会自动把相应的内核对象中的这个布尔值设为TRUE,表示该对象已经被触发了。如果我们想要检查一个进程是否正在运行,那么我们只需要调用一个函数,让操作系统来检查进程对象的布尔值就可以了。
6、线程内核对象也同进程内核对象一样。
7、下面的内核对象既可以处于触发状态,也可以处于未触发状态:进程、线程、作业、文件以及控制台的标准输入流/输出流/错误流、事件、可等待的计时器(waitable timer)、信号量、互斥量。用来决定每个对象处于触发状态还是未触发状态的规则与对象的类型有关。
8、Windows运行一个线程等待某个特定的内核对象被触发的函数。还有提供专门用来帮助进行线程同步的内核对象:事件、可等待计时器、信号量以及互斥量。
9、当线程正在等待的对象处于未触发状态的时候,它们是不可调度的。但是,一旦对象被处罚了,那么线程就变成可调度状态,然后继续执行。(触发状态比作升旗)

一、等待函数

1、等待函数使一个线程自愿进入等待状态,知道指定的内核对象被触发为止。如果调用一个等待函数的时候,相应的内核对象以及处于触发状态,那么线程是不会进入等待状态的。
2、最常用的是WaitForSingleObject函数,第一个参数是内核对象,第二个参数是用来指定线程最多愿意花多长的时间来等待对象被触发。(单位为微妙)如果用WaitForSingleObject(hProcess,INIFINITE)则使调用线程一直等待直到hProcess句柄标志的进程终止为止。WaitForSingleObject的返回值表示为什么调用线程又能够继续执行了。WAIT_OBJECT_0表示等待的对此被触发;WAIT_TIMEOUT表示等待超时;WAIT_FAILED表示传入参数(比如一个无效的句柄)。
3、WaitForMultipleObject和Single的相似,唯一不同之处在于它允许调用线程同时检查多个内核对象的触发状态。(http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx

DWORD WINAPI WaitForMultipleObjects(
  __in  DWORD nCount,
  __in  const HANDLE *lpHandles,
  __in  BOOL bWaitAll,
  __in  DWORD dwMilliseconds
);

count参数是希望函数检查的内核对象的数量,phObjects是一个内核对象句柄数组指针。接下来的标志可以说明函数工作模式:一种是让线程进入等待状态直到内核对象中的一个被触发为止,另一种是让线程进入等待状态直到指定内核对象中的全部被触发为止。返回值和Single类似,多了返回值是WAIT_OBJECT_0+dwCount-1之间的任何一个值,减去WAIT_OBJECT_0就能得到触发的是那个对象了。(数组索引)

二、等待成功所引起的副作用

1、对一些内核对象来说,成功地调用WaitForSingleObject或WaitForMultipleObject事实上会改变对象的状态。一个成功的调用指的是函数发现对象已经被触发了,然后返回WAIT_OBJECT_0的一个相对值。如果对象的状态发生了改变,则称之为等待成功所引起的副作用(successful wait side effects)
2、举个列子,假设线程正在等待一个自动重置事件对象(auto-reset event object)。当事件对象被触发的时候,函数会检测到这一情况,这时它可以直接返回WAIT_OBJECT_0给调用线程。但是,就在函数返回之前,它会使事件变为非触发状态——这就是等待成功所引起的副作用。
3、自动重置事件内核对象之所以会有这样的副作用,其原因是,这时MS为此类对象定义的诸多规则中的一条。其他对象有不同的副作用,有些对象则完全没有副作用。
4、WaitForMultipleObject之所以这么有用的原因,是因为它能够以原子方式执行所有操作。当调用该函数的时候,函数会测试所有对象的触发状态,并引发相应的副作用,所有这些都是作为一个操作来完成的。这就可以防止一定的死锁情况。当函数检查内核对象的状态时,任何其他线程都不能再背后修改对象的状态。
5、多个线程在等待同一个内核对象,当对象被触发的时候,系统会唤醒哪个线程?MS的官方回答是“算法是公平的”。这意味着线程优先级将没有效果,优先级最高的线程不一定能得到对象。(应该明确优先级的主要作用吧,用于CPU调度,从可调度到运行。而不是从等待到可调度)MS所使用的算法只不过是“先入先出”机制。等待时间最长的线程得到对象,但是内部具体的流程无法明确。但是,不会调度对象给一个被挂起的线程。只有当后来线程恢复的时候,系统会认为这个线程才刚刚开始等待对象。

三、事件内核对象

1、在所有内核对象中,事件比其他对象要简单得多。事件保护一个使用计数,一个用来表示事件是自动重置事件还是手动重置事件的布尔值,以及另一个用来表示事件有没有被触发的布尔值。
2、事件的触发表示一个操作已经完成。有两种不同类型的事件对象:手动重置事件和自动重置事件。当一个手动重置事件触发的时候,正在等待该事件的所有线程都将变成可调度状态。而当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调度状态。
3、事件最通常的用途是,让一个线程执行初始化工作,然后再触发另一个进程,让它执行剩余的工作。一开始将事件初始化为未触发状态,然后当线程完成初始化工作的时候,触发事件。此外,另一个线程一直在等待该事件,它发现事件被触发,于是变成可调度状态。第二个线程知道第一个线程已经完成了它的工作。
4、CreateEvent函数,(http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx

HANDLE WINAPI CreateEvent(
  __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,
  __in      BOOL bManualReset,
  __in      BOOL bInitialState,
  __in_opt  LPCTSTR lpName
);

5、还有CreatEventEx创建对象,(http://msdn.microsoft.com/en-us/library/windows/desktop/ms682400(v=vs.85).aspx

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

倒数第二个参数是标志掩码

最后一个参数允许我们指定在创建事件时返回的句柄对事件有何种访问权限。这是一种创建事件句柄的新方法,可以减少权限。相比较而言,CreateEvent总是被授予全部权限。但CreateEventEx更有用的地方在于它允许我们以减少权限的方式打开一个已经存在的事件,而CreateEvent总是要求全部权限。
6、其他进程中的线程可以通过多种方式来访问该事件对象,这包括调用CreateEvent并在pszName参数中传入相同的值,使用继承,使用DuplicateHandle函数,或者调用OpenEvent并在pszName参数中指定与CreateEvent中相同的名字。一如既往,应该在不再需要事件内核对象的时候调用CloseHandle函数来将它关闭。
7、一旦创建了事件,就可以直接控制它的状态。调用SetEvent设置为触发状态;调用ResetEvent设置为未触发状态。
8、MS为自动重置事件定义了一个等待成功所引起的副作用:当线程成功等到自动重置事件对象的时候,对象会自动地重置为未触发状态。
9、处于完整性考虑,还有一个可以用于事件的函数:PulseEvent。PulseEvent会先触发事件然后立刻将其恢复到未触发状态,这就相当于在调用SetEvent之后立即调用ResetEvent一样。用处不大,因为根本不知道到底会不会有线程发现这个触发脉冲。即使有,也不知道哪个线程会发现这个触发脉冲并变为可调度状态。
10、http://msdn.microsoft.com/en-us/library/windows/desktop/ms686293(v=vs.85).aspx

四、可等待的计时器内核对象

1、可等待的计时器是这样一种内核对象,会在某个指定的时间触发,或每隔一段时间触发一次。
2、创建可等待的计时器,调用CreateWaitableTimer函数。OpenWaitableTimer函数用来获得一个已经存在的可等待计时器。在创建的时候,可等待的计时器对象总是处于未触发状态。当我们想要触发计时器的时候,必须调用SetWaitableTimer函数。

3、SetWaitableTimer原型(http://msdn.microsoft.com/en-us/library/windows/desktop/ms686289(v=vs.85).aspx

BOOL WINAPI SetWaitableTimer(
  __in      HANDLE hTimer,
  __in      const LARGE_INTEGER *pDueTime,
  __in      LONG lPeriod,
  __in_opt  PTIMERAPCROUTINE pfnCompletionRoutine,
  __in_opt  LPVOID lpArgToCompletionRoutine,
  __in      BOOL fResume
);

pDueTime表示计时器第一次触发的时间应该在什么时候,参数LPeriod表示在第一次触发之后,计时器应该以怎样的频度触发。(LARGE_INTEGER、SYSTEMTIME、FILETIME结构)
4、在调用SetWaitableTimer的时候,还可以指定一个相对时间。传入pDueTime参数为负值,且必须是100纳秒的整数倍。
5、给lPeriod参数传0,就可以实现一次性计时器。这种计时器只触发一次,之后再也不触发。然后调用CloseHandle来关闭计时器或者再次调用SetWaitableTimer来重置计时器,给它设置一个新的触发时间。
6、CancelWaitableTimer会吧句柄所标识的计时器取消,这样计时器就永远不会被触发了,除非以后再调用SetWaitableTimer来对它进行重置。
7、MS允许计时器把一个异步过程调用(asynchronous procedure call,APC)放到SetWaitableTimer的调用线程的队列中。
8、一般pfnCompletionRoutine和pvArgToCompletionRoutine两个参数传入NULL,时间一到应该触发计时器对象。但是,如果希望时间一到把一个APC添加到队列中去,就必须实现一个计时器APC函数,并把函数的地址传入。(http://msdn.microsoft.com/en-us/library/windows/desktop/ms681947(v=vs.85).aspx

VOID CALLBACK APCProc(
  __in  ULONG_PTR dwParam
);

9、计时器被触发的时候,当且仅当SetWaitableTimer的调用线程正处于可提醒状态(alertable state)时,这个函数会被同一个线程调用。换句话说,线程必须是由于调用SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectEx、MsgWaitForMultipleObjectEx或SignalObjectAndWait而进入的等待状态。如果线程并非在其中的一个函数内等待,那么系统不会把计时器的APC函数添加到队列中。这可以避免线程的APC队列因为计时器的APC通知而负荷过多,从而避免浪费系统中的大量内存。
10、当计时器被触发的时候,如果线程处于可提醒状态,系统会让线程调用回调函数。回调函数的第一个参数的值与我们传给SetWaitableTimer函数的pvArgToCompletionRoutine参数的值相同,剩下两个参数,表示计时器被触发的时间。
11、只有当所有的APC函数都处理完毕后,才会返回可警告函数(alertabe function)。因此,我们必须确保自己的TimerAPCRoutine函数会在计时器再次被触发之前结束,不然APC函数加入队列的速度就快过了处理它们的速度。
12、线程不应该在等待一个计时器句柄的同时以可提醒的方式等待同一个计时器。(?)
13、计时器被频繁用于通信协议。例如,如果客户想服务器发送一个请求而服务器未能在一定时间内向响应,那么客户可以认为服务器不可用。
14、计时器可以将APC函数添加到线程的队列中,这点虽好,但现今大多数的应用程序并没有使用APC,而是使用I/O完成端口(I/O completion port)机制。
15、可等待计时器和用户计时器(通过SetTimer函数设置),两者最大的区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。此外,可等待计时器是内核对象,这意味着它们不仅可以在多个线程间共享,而且可以具备安全性。用户计时器会产生WM_TIMER消息,这个消息被送回调用SetTimer的线程(对回调计时器来说),或者被送回创建窗口的线程(对基于窗口的计时器来说)。因此,当一个用户计时器触发的时候,只有一个线程会得到通知。另一方面,多个线程可以等待可等待计时器,如果计时器是手动重置计时器,那么有多个线程可以变成可调度状态。
16、使用可等待计时器,更有可能是因为等待超时而得到通知。WM_TIMER消息总是优先级最低的,只有当线程的消息队列中没有其他消息的时候才会被处理。

五、信号量内核对象

1、信号量内核对象用来对资源进行计数。与其他所有内核对象相同,它们也包含一个使用计数,但它们还包含另外两个32位值:一个最大资源计数和一个当前资源计数(可被使用资源数目)。最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量。
2、信号量的规则如下:
a、如果当前资源计数大于0,那么信号量处于触发状态;
b、如果当前资源计数等于0,那么信号量处于未触发状态;
c、系统绝对不会让当前资源计数变为负数;
d当前资源计数绝对不会大雨最大资源计数。
3、不能把信号量对象的使用计数和它的当前资源计数混为一谈。
4、CreateSemaphore和CreateSemaphoreEx创建,OpenSemaphore打开。
5、信号量的最大的优势在于它们会以原子方式来执行这些测试和设置操作,也就是说,当我们向信号量请求一个资源的时候,操作系统会检查资源是否可用,并将可用资源的数量递减,整个过程不会被别的线程打断。只有当资源计数递减完成之后,系统才会允许另一个线程请求对资源的访问。
6、线程通过调用ReleaseSemaphore来递增信号量的当前资源计数。

六、互斥量内核对象

1、互斥量(mutex)内核对象用来确保一个线程独占对一个资源的访问。这也是互斥量名字的由来。互斥量对象保护一个使用计数、线程ID以及一个递归计数。互斥量与关键段的行为完全相同。但是,互斥量是内核对象,而关键段是用户模式下的同步对象。(除非对资源的争夺非常激烈,这种情况下关键段的线程将不得不进入内核模式等待)这意味着互斥量比关键段慢。但这同时意味着不同进程中的线程可以访问同一个互斥量,还意味着线程可以在等待对资源的访问权时指定一个最长等待时间。
2、线程ID用来标识当前占用这个互斥量的是系统中的哪个线程,递归计数表示这个线程占用该互斥量的次数。互斥量一般用来对多个资源访问的同一块内存进行保护。
3、互斥量的规则:
a、如果线程ID为0(无效线程ID),那么该互斥量不为任何线程锁占用,它处于触发状态;
b、如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发状态;
c、与所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。
4、CreateMutex和CreateMutexEx用来创建,OpenMutex用来得到一个已经存在的互斥量的句柄,该句柄与当前进程想关联。
5、在用来触发普通内核对象和撤销触发普通内核对象的规则中,有一条不适用于互斥量。假设线程试图等待一个未触发的互斥量对象。在这种情况下,线程通常会进入等待状态。但是,系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果线程ID一致,那么系统会让线程保持可调度状态——即使该互斥量尚未触发。对系统中的任何其他内核对象来说,都找不到这种异常的举动。每次线程成功地等待了一个互斥量,互斥量对象的递归计数会递增。使递归计数大于1的唯一途径是利用这个例外,让线程多次等待同一个互斥量。
6、互斥量与所有其他内核对象不同,这是因为它们具有“线程所有权”的概念。除了互斥量,没有任何一个会记住自己是哪个线程等待成功的。互斥量的这种线程所有权的概念,也是它具有特殊规则的原因,这使它即使在未触发的状态下,也能为线程所获得。
7、这个例外不仅适用于获得互斥量的线程,而且适用于试图释放互斥量的线程。当线程调用ReleaseMutex的时候,会缉拿吃线程的线程ID与互斥量内部保存的线程ID是否一致。如果ID一致,则递归计数会递减。如果ID不一致,则不执行任何操作,返回FALSE。
8、如果占用互斥量的线程在释放互斥量之前终止(使用ExitThread,TerminateThread,ExitProcess或TerminateProcess),那么系统会认为互斥量被遗弃(abandoned)——由于占用互斥量的线程已经终止,因此再也无法释放它。
9、系统会记录所有的互斥量和线程内核对象,因此它确切地知道互斥量何时被遗弃。当互斥量被遗弃的时候,系统会自动将互斥量对象的线程ID设为0,将它的递归计数设为0.然后再检查有没有其它线程正在等待该互斥量。被调度的线程的等待函数返回的是一个特殊的值WAIT_ABANDONED。这个特殊的返回值只适用于互斥量。
10、互斥量和关键段的比较

七、线程同步对象速查表

1、内核对象与线程同步

八、其他的线程同步函数

1、异步设备I/O(asynchronous device I/O)允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。
2、设备对象是可同步的内核对象,这意味着可以调用WaitForSingleObject,并传入文件句柄、套接字、通信端口,等等。
3、WaitForInputIdle函数将自己挂起,会等待由hProcess标志的进程,知道创建应用程序第一个窗口的线程中没有待处理的输入为止。这个函数对父进程来说比较有用。
4、MsgWaitForMultipleObjects或MsgWaitForMultipleObjectsEx使得线程等待需要自己处理的消息。与WaitForMultipleObjects函数类似,不同之处在于,不仅内核对象被触发的时候调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。
5、创建窗口的线程和执行与用户界面相关的任务的线程不应该使用WaitForMultipleObjects,而应该使用MsgWaitForMultipleObjectsEx。这是因为前者会妨碍线程对用户在用户界面上的操作进行响应。
6、WaitForDebugEvent函数,可以用来等待调试事件发生。
7、SignalObjectAndWait函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象。触发的内核对象必须是一个互斥量、信号量或事件。等待的可以是任何一种内核对象。
8、SignalObjectAndWait受欢迎有两个原因:一个是节省时间,只用进入内核一次,否则先release一次,然后又要wait一次;另一个是如果没有该函数,一个线程就无法知道另一个线程合适处于等待状态。
9、使用等待链遍历API来检测死锁,Vista提供一组新的等待链遍历(Wait Chain Traversal,WCT)API。

抱歉!评论已关闭.