速度快 局限性 容易死锁
内核对象实现同步 适应性高 速度慢(需要用户转内核,1000个cpu周期)
(进程 线程 作业 内核对象 )都处于已通知和未通知的状态之中。
进程在创建的时候是未通知,终止是已通知;得到通知就不会再改变。
进程内核对象是的布尔值,在创建的时候是FALSE(未通知状态),终止时该值自动被置为TRUE,表示已经得到通知。
如果代码是检查进程是否在运行,那么只需要调用一个函数,让操作系统去检查进程内核对象的布尔值,当布尔值从FALSE变为TRUE时自动唤醒改线程。这样只要编译代码使你的进程进入等待状态,标识子进程的内核对象变为已通知即可。Windows提供了一些方便完成这些操作的函数。
线程也有和进程一样的规则,创建时未通知,终止时已通知。拥有这个属性的有进程、线程、文件修改通知、事件、作业、可等待定时器、文件、新标、控制台输入、互斥对象。
线程可以让自己进入等待状态,知道一个对象变为已通知状态。
9.1 等待函数
最常用的等待函数:WaitForSingleObject。第一个参数标识一个能够支持被通知和未通知的内核对象。第二个参数指明线程为了等待对象变为已通知状态,它愿意等的时间,如果是INFINITE(定义为-1或者0xFFFFFFFF),线程将永远等下去(浪费CPU时间)。函数的返回值是个DWORD,用来表示线程为什么重新变为可调度状态,常用的有WAIT_TIMEOUT、WAIT_FAILED(原因查询GetLastError)、WAIT_OBJECT_0。
相似的函数WaitForMultipleObjects,允许调用的线程查看若干个内核对象的通知状态。第一个参数dwCount的范围在1到MAXIMUN_WAIT_OBJECTS(Windows头文件中定义为64),phObjects是指向内核对象数组的指针。
两种方式来使用WaitForMultipleObjects:
- 让线程进入等待状态,直到其中任何一个内核对象变为已通知状态。
- 线程进入等待状态,知道所有内核对象变为已通知状态。
如果fWaitAll为TRUE,在所有对象变为已通知状态之前,线程保持等待状态。dwMilliseconds参数的作用和之前的函数的同名函数相同,如果规定的时间到了,线程无论如何都要返回。通常为它传递INFINITE,但是小心死锁的情况。返回值同样是解释线程可以被重新调度的原因,可以是WAIT_FAILED和WAIT_TIMEOUT。如果fWaitAll是FALSE,那么一旦其中一个内核对象变为已通知状态,函数就返回,返回值是WAIT_OBJECT_0与WAIT_OBJECT_0+dwCount-1之间的值。
9.2 等待成功的副作用
如果WaitForMultipleObjects和WaitForSingleObject的返回值不是WAIT_TIMEOUT也不是WAIT_FAILED,对象的状态将会被改变,这就是成功等待的副作用。它用于自动清除内核对象,这是Microsoft为这种类型的对象定义的规则之一。其他的对象有不同的副作用,有些没有副作用。
由于WaitForMultipleObjects是使用原子操作的方式执行它所有的操作,能够测试所有对象的状态,并根据需要选择其中的一项或者全部作为结果来执行。而且当它检查内核对象的状态时,其他任何线程都不能改变它的状态,这可以防止出现死锁情况。
这也有个问题,有多个线程等待单个内核对象,那么当这个内核对象变成已通知状态的时候,系统会唤醒哪个线程呢?Microsoft的答复是:每个线程都有同样的机会获得已通知状态恢复运行。
但也意味着线程的优先级别不起作用了,而且等待时间最长的线程不一定得到该对象,甚至得到对象的线程都有可能反复循环而得到该对象。所以这对其他线程来说其实是不公平的。
在实际操作中,Microsoft使用的是“先进先出”的方案,等待了最长时间的线程将得到该对象, 但是系统又有其他的一些操作用来改变这个特性,难以预测,这也是Microsoft没有说清楚算法的细节的原因。其中一个操作就是线程暂停并等待一个对象后系统会忘记线程在等待对象,到线程恢复运行的时候,系统将认为线程刚刚开始等待该对象。
当调试一个进程时,只要达到一个断点,所有线程都会暂停。所以线程常常暂停运行,又再恢复运行,这也是系统使用“先进先出”的算法会难以预测结果的原因。
9.3 事件内核对象
事件也是一个中内核对象,包括一个引用计数,一个指明这个事件是自动重置还是人工重置的布尔值。人工重置的事件得到通知,等待该事件的所有线程都变味可调度的;自动重置的事件得到通知,等待该事件的线程中只有一个线程变为可调度线程。
两个线程之间的通信方式:
- 事件初始化为未通知状态;
- 一个线程完成初始化操作,把事件设置为已通知状态
- 另一个线程得到通知,完成剩下的操作。
CreateEvent函数,用来创建事件内核对象,按照第三章的介绍,它可以设置安全性、引用计数、继承它的句柄,按名字共享对象。
所以先是fManualReset这个布尔值,TRUE表示是个人工重置的事件,FALSE表示是个自动重置的事件。fInitialState也是布尔值,TRUE表示已通知状态,FALSE表示未通知状态。
返回值是内核对象句柄,用以获得对该对象的访问权。方法第三章已经介绍过。
通过函数SetEvent可以将事件设置为已通知状态,ResetEvent让事件回到未通知状态。对于自动重置的事件类型,通常没有必要调用ResetEvent。
函数PulseEvent使得事件变为已通知状态,然后又立即变成未通知状态,就像调用SetEvent马上调用ResentEvent一样。如果自动重置的事件调用了它, 就好像没效果一样。本书的作者也表示用处并不大。
9.4 等待定时器内核对象
在某个时间或者规定的时间发出自己的信号通知的内核对象,通常用来在某个时间执行某个操作。创建定时器的函数是CreateWaitableTimer,参数同样有安全属性的指针和字符指针用以支持继承属性,创建和自己进程相关的定时器使用OpenWaitableTimer。SetWaitableTimer用来把内核对象变成已通知状态。
SetWaitableTimer的参数很多,唯一一个需要特别注意的的第二个参数,那是个LARGE_INTEGER的指针,它的边界使用64位开始的,只不过x86处理器下转化的时候这个bug体现不出来。书中提到有效的操作是把FILETIME的成员拷贝到LARGE_INTEGER的成员里,然后把后者传递给函数SetWaitableTimer。
9.4.1让定时器给APC排队
一般来说,给SetWaitableTimer的pfnCompletionRoutine和pvArgCompletionRoutine传递NULL就可以,这样届时规定的时间到了,函数就知道向定时器发出信号。可是你有时回想让定时器给一个APC排队,那么就要传递APC例程的地址,和线程函数类似,这个地址是你必须实现的。
现在还没做完准备工作,线程必须在WaitForMultipleObjects、WaitForSingleObjectEx、SleepEx、MsgWaitForMultipleObjects或者SingleObjectAndWait等函数的调用中等待。如果该线程不再这些函数中的其中某个等待,系统是不会给定时器apc排队的,防止线程的APC队列中塞满APC通知,浪费大量的系统内存。
只有当所有的APC项都已经处理之后,待命的函数才会返回,因此必须保证定时器再次变为已通知状态之前,pfnCompletionRoutine已经完成了它的运行。
9.4.2 定时器的松散性
定时器常见于通信协议中,但是又由于比较使用起来比较麻烦而很少的应用程序会使用它,而是使用新的线程共享函数中的一个:CreateTimerQueueTimer。如果观察这个函数,应用程序将会减少开销。
给APC项排序的更好办法是IO完成端口机制。
等待定时器和用户定时器相比,后者需要许多附加的用户界面结构,使得它变得资源更加密集,而前者是内核对象,意味着可以多线程共享,而且是安全的;用户定时器通过生成WM_TIMER消息并发送给SetTimer的线程和创建窗口的线程,因此只有一个线程能得到通知,而等待定时器可以有多个线程在它上面等待,如果是个人工重置的类型,还可以调度不止一个线程。
当然要执行和用户界面相关的时间,当然是使用用户定时器方便。即便到了规定的时间,等待定时器更有可能得到通知,毕竟用户定时器是个比较低优先级的消息。
9.5 信标内核对象
除了内核对象都有的引用计数,信标还有两个带符号的32位值,分别记录最大资源数量和当前资源数量。信标的使用规则如下:
- 如果当前资源的数量大于0,则发出信标信号。
- 如果当前资源的数量是0,则不发出信标信号。
- 系统不允许当前资源是数量是负值。
- 当前资源数量不能大于最大资源数量。
HANDLE CreateSemaphore(PSECURITY psa, LONG lInitialCount, lMaximumCount, PCTSTR pszName);
HANDLE OpenSemaphore(DWORD fdwAcess, BOOL bInheritHandle, PCTSTR pszName);
BOOL ReleaseSemaphore(HANDLE hsem, LONG lReleaseCount, PLONG plPreviousCount);
9.6 互斥对象内核对象
互斥对象(mutex)能够确保线程拥有对单一资源的互斥访问权。其特性和关键代码段相同,只是它内核对象,相对较忙,但是也能被多个进程中的多个线程访问。互斥对象包含一个引用计数、一个线程ID和一个递归计数器。ID用来指明系统中的哪个线程拥有互斥对象,递归计数器指明改线程拥有的次数。很常用,多用于保护多个进程同时要访问的内存块,保证对资源的独占权和数据的完整性。
使用规则:
- 如果线程ID是0,互斥对象不被任何线程所拥有,并且发出该互斥对象的通知信号。
- 如果线程ID非0,那么就有一个线程拥有这个互斥对象,并且不发出通知信号。
- 不同于其他的内核对象,互斥对象在内核中的代码比较特殊,允许它们违反正常的规则。
使用互斥对象就要调用CreateMutex:
HANDLE CreateMutex(
PSECURITY psa, BOOL bInitialOwner, PCTSTR pszName
);
HANDLE OpenMutex (DWORD fdwAcess, BOOL bInheritHandle, PCTSTR pszName);
BOOL ReleaseMutex(HANDLE hMutex);
9.6.1 释放问题
互斥对象之所以特殊是因为它有一个“线程所有权”的概念,只有这个内核对象可以知道是哪一个线程成功等到了该对象,即使线程没有发出通知。
这也使得调用ReleaseMutex的时候线程可以判断调用函数的线程ID是不是正确的ID,只有匹配的时候才会有正常的操作,否则返回FALSE给调用者,调用GetLastError得到的信息则是ERROR_NOT_OWNER(试图释放不是调用者拥有的互斥对象)。
如果拥有互斥对象的线程在互斥对象被销毁前终止运行,系统就把这个互斥对象视为已经放弃,反正拥有它的线程已经gg了。
系统了解互斥对象的情况,知道它何时被放弃。到那个时候,系统就把互斥对象的ID置零、递归计数器置零,然后还有查看是否有其他的线程在等待这个对象。若有,系统还会公平地选择一个等待线程,设置将互斥对象的ID设置为线程的ID,并将递归计数器设为1,最后还好把这个线程变为可调度线程。
特殊的返回值WAIT_ABANDONED,指明线程在等待的对象是另一个进程拥有的。但实际情况下这个值并不多被检查,毕竟很少线程是刚刚终止运行的。(这也是不应该适用TerminateThread函数终止线程的原因)
9.7 线程同步对象小结
内核对象与进程同步之间的关系:
对象 |
何时处于未通知状态 |
何时处于已通知状态 |
成功等待的副作用 |
进程 |
进程仍在活动时 |
进程终止 |
无 |
线程 |
线程仍在活动时 |
线程终止 |
无 |
作业 |
作业时间未结束 |
作业时间结束 |
无 |
文件 |
IO请求处理中 |
IO请求处理完毕 |
无 |
控制台输入 |
不存在输入 |
存在输入 |
无 |
文件修改通知 |
没有文件被修改 |
文件系统发现修改 |
重置通知 |
自动重置事件 |
ResetEvent,PulseEvent,等待成功 |
SetEvent,PulseEvent |
重置事件 |
人工重置事件 |
ResetEvent,PulseEvent |
SetEvent,PulseEvent |
无 |
自动重置等待定时器 |
CancelWaitableTimer或等待成功 |
时间到时SetWaitableTimer |
重置定时器 |
人工重置等待定时器 |
CancelWaitableTimer |
时间到时SetWaitableTimer |
无 |
信标 |
等待成功 |
未被线程拥有时 |
数量减一 |
互斥对象 |
等待成功 |
未被线程拥有时 |
将所有权赋予线程 |
关键代码段 |
((Try)EnterCriticalSection) |
LeaveCriticalSection |
将所有权赋予线程 |
9.8 其他的线程同步函数
9.8.1 异步设备IO
异步设备IO使得线程能够启动一个读(写)操作,但不必等待操作完成,而可以完成自己的操作,线程就可以终止自己的运行,而等待系统通知它文件已经读取。是支持同步的内核对象,可以用WaitForSingleObject函数去等待它完成。
9.8.2 WaitForInputIdle
线程也可以用WaitForInputIdle来终止自己的运行。它有两个参数,一个是进程的句柄,一个是等待的时间。函数一直保持等待状态,直到hProcess标识的进程在创建应用程序的第一个窗口的线程中已经没有尚未处理的输入为之。父进程想知道子进程何时完成初始化可以用到;当需要将击键的输入纳入应用程序时,也可以用到。比如说WM_KEYDOWN消息。
9.8.3 MsgWaitForMultipleObjects(Ex)
线程可以用这个函数等待它自己的消息。这个函数与WaitForMultipleObjects函数的差别在于这个函数允许线程在内核对象变成已通知状态,或者窗口消息需要调度到现场创建的窗口时被调度。
创建窗口与执行与用户界面相关的任务的线程,应该使用MsgWaitForMultipleObjectsEx,使得用户界面保持对用户做出反应。
9.8.4 WaitForDebugEvent
Microsoft支持调试的特性,当调试程序启动时,它将自己附加给一个被调试程序,自己只是闲着,等待操作系统和被调试程序的相关调试事件通知它,用这个函数可以等待这些事件的发生。
当调试程序调用这个函数时,调试程序的线程终止运行,系统将与调试事件相关的调试事件通知它。
9.8.5 SignalObjectAndWait
函数用于在单个原子操作中发出关于内核对象的通知并等待另一个内核对象。
DWORD SignalObjectAndWait(
HANDLE hObjectToSignal,
HANDLE hObjectToWaitOn,
DWORD dwMillisecond,
BOOL fAlertable);
hObjectToSignal标志一个互斥对象、信标或者事件,其他类型对象则返回WAIT_FAILER,而且GetLastError返回ERROR_INVALID_HANDLE。然后再内部,函数就会根据对象的类型调用相应的撤销内核对象的函数。
hObjectToWaitOn标志一个内核对象类型可以是互斥对象、新标、事件、定时器、进程、线程、作业、控制台输入和文件修改通知。
dwMillisecond指明为了等待对象变为已通知状态,线程应该等待多长时间。
fAlertable则指明线程等待改内核对象时是否能够处理任何已经排队的异步过程调用。
返回值是:WAIT_OBJECT_0、WAIT_TIMEOUT、WAIT_FAILED、WAIT_ABANDONED、WAIT_IO_COMPLETION。
在高性能服务器程序中,SignalObjectAndWait能够节省大量的时间。