在用户模式下进行线程同步的好处就是速度快。如果关心应用程序的性能,则应该优先考虑在用户模式下的同步机制能否适用。
内核对象都有触发和未触发两种状态,微软创建了一些规则,用来规定如何在这两种状态之间进行转换。比如,进程(线程)内核对象在创建的时候总是处于未触发状态。当进程(线程)终止的时候,操作系统会自动使进程(线程)内核对象变成触发状态。当进程(线程)内核对象被触发后,它将永远保持这种状态,再也不会变回到为触发状态。
等待函数使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。如果线程在调用一个等待函数时,相应的内核对象已经处于触发状态,那么线程是不会进入等待状态的。
WaitForSingleObject(HANDLE hObject,DWORD dwMillseconds)是最常用的等待函数。该函数的返回值表示为什么调用线程又能够继续执行。如果是因为线程等待的对象被触发,那么返回值是WAIT_OBJECT_0;如果是因为等待超时,那么返回值是WAIT_TIMECOUT。如果给该函数传入了无效参数,那么返回值是WAIT_FAILED(这时可以调用GetLastError得到更详细的信息)。
如果允许调用线程同时检查多个内核对象的触发状态,我们可以调用函数WaitForMultipleObjects。可以通过两种不同方式来使用这个函数:一种是让线程进入等待状态直到指定内核对象中的一个被触发为止,另一种是让线程进入等待状态直到指定内核对象中的全部被触发为止。如果返回值既不是WAIT_TIMECOUT,也不是WAIT_FAILED,那么我们应该把返回值减去WAIT_OBJECT_0。得到的数值是我们在第二个参数中传递的句柄数组的索引,用来告诉我们被触发的是哪个对象。
WaitForMultipleObjects是以原子方式工作的。当函数检查内核对象的状态时,任何其他线程都不能再背后修改对象的状态。这样可以防止死锁情况的发生。
多个线程等待同一个内核对象时,那么当对象被触发时,系统如何决定应该唤醒哪个线程?微软回答是:算法是公平的,也就是说每个线程都有可能被唤醒。与线程优先级以及等待时间长短都没有关系。但是实际上,微软使用算法的机制是“先入先出”。
事件内核对象包含一个使用计数,一个用来表示事件是自动重置事件还是手动重置事件的布尔值,以及另一个用来表示事件有没有被触发的布尔值。当一个手动重置事件被触发的时候,正在等待该事件的所有线程都将变成可调度状态,而当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调度状态。
事件最通常的用途是,让一个线程执行初始化工作,然后再触发另一个线程,让它执行剩余工作。开始我们将事件初始化为未触发状态,然后当线程完成初始化工作的时候,触发事件。此时另一个线程一直等待该事件,它发现事件被触发,于是变成可调度状态。
函数PulseEvent会先触发事件然后立刻将其恢复到未触发状态,相当于在调用SetEvent之后立即调用ResetEvent一样。
可等待计时器是一种内核对象,它会在某个指定的时间触发,或每隔一段时间触发一次。它们通常用来在某个时间执行一些操作。要创建可等待的计时器,可以调用CreateWaitableTimer函数。也可以调用OpenWaitableTimer函数来得到一个已经存在的可等待计时器的句柄,该句柄与当前进程相关联。可以创建手动重置或自动重置的计时器。当手动重置计时器被触发时,正等待在该计时器的所有线程都变成可调度状态;当自动重置计时器被触发时,只有一个正等待在该计时器的线程会变成可调度状态。
可等待计时器在创建时,总是处于未触发状态。如果要想触发计时器,我们可以调用SetWaitableTimer函数。可以在该函数中指定计时器第一次触发的时间,以及之后计时器以怎样的频度触发。第一次触发的时间可以是绝对时间,也可以指定一个相对时间。如果希望计时器只触发一次,可以将触发频度参数传0值,然后调用CloseHandle关闭计时器。
调用CancelWaitableTimer会把句柄标识的计时器取消,这样计时器就永远不会触发了,除非后来再调用SetWaitableTimer来对它进行重置。
微软允许计时器把一个异步过程调用APC放到SetWaitableTimer的调用线程的队列中。只需要实现一个计时器的APC函数,将函数的地址传给SetWaitableTimer的参数pfnCompletionRoutine。当计时器被触发的时候,当且仅当SetWaitableTimer的调用线程正好处于可提醒状态时(即线程必须是由于调用SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectEx、MsgWaitForMultipleObjectsEx、SignalObjectAndWait而进入的等待状态),这个APC函数才会被调用线程调用。
线程不应该在等待一个计时器句柄的同时以可提醒的方式等待同一个计时器。
可等待计时器和用户计时器(通过SetTimer函数来设置)的最大区别:用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。而可等待计时器是内核对象,它可以在多个线程间共享,而且具备安全性。
信号量内核对象用来对资源进行计数。它包含一个使用计数,还包含另外两个32位值:一个最大资源计数和一个当前资源计数。最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量。可以使用CreateSemaphore或CreateSemaphoreEx来创建一个信号量,可以使用OpenSemaphore来得到一个已经存在的信号量的句柄。
信号量的规则:
● 如果当前资源计数大于0,那么信号量处于触发状态
● 如果当期资源计数等于0,那么信号量处于未触发状态
● 系统绝对不会让当前资源计数变为负数
● 当期资源计数绝对不会大于最大资源计数
线程通过调用ReleaseSemaphore来递增信号量的当前资源计数。我们无法在不改变当前资源数的前提下来得到信号量的当前资源计数值。
互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量对象包含一个使用计数、线程ID以及一个递归计数。互斥量与关键段的行完全相同。互斥量是内核对象,关键段时用户模式下的同步对象,所以互斥量比关键段慢。同时不同进程中的线程可以访问同一个互斥量,线程可以在等待对资源的访问权时指定一个最长等待时间,关键段则做不到这些。线程ID用来标识当前占用这个互斥量的是系统中的哪个线程,递归计数表示这个线程占用该互斥量的次数。
互斥量的规则:
● 如果线程ID为0,那么该互斥量不为任何线程所占用,它处于触发状态。
● 如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发状态。
● 与所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规。
函数CreateMutex用来创建一个互斥量。也可以调用OpenMutex来得到一个已经存在的互斥量的句柄,该句柄与当前进程相关联。
系统检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果线程ID一致,那么系统会让线程保持可调度状态—即使该互斥量尚未出发。每次线程成功等待了一个互斥量,互斥量对象的递归计数会递增。使递归计数大于1的唯一途径是利用这个例外,让线程多次等待同一个互斥量。
如果当前占有访问权的线程不再需要访问资源的时候,必须调用ReleaseMutex函数来释放互斥量。这个函数会将对象的递归计数减1。如果线程成功地等待了互斥量对象不止一次,那么线程必须调用ReleaseMutex相同的次数才能使对象的递归计数变为0。当递归计数变为0的时候,函数还会将线程ID设为0,这样就会触发该互斥量对象。
互斥量对象与其他内核对象不同,它们具有“线程所有权”的概念。除了互斥量,没有任何一个内核对象会记住自己是哪个线程等待成功的。这种特殊性使得互斥量即使在未触发的状态下,也能为线程所获得。如果占用互斥量的线程在释放互斥量之前终止,系统会认为互斥量被遗弃了,这时系统会自动将互斥量对象的线程ID设为0,将它的递归计数设为0,然后检查有无其他线程正在等待该互斥量。如果有,系统会公平选择一个正在等待的线程,把对象内部的线程ID设为所选择线程的线程ID,把递归计数设为1,这样被选择的线程变为可调度状态。
线程同步对象速查表
注意:用户模式下的Interlocked系列函数不会使线程变成不可调度状态,它们只是修改一个值并立即返回。
异步设备I/O允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。设备对象是可同步的内核对象,我们可以调用WaitForSingleObject并传入文件句柄、套接字、通讯端口等等。当系统异步执行I/O时,设备对象处于未触发状态。而一旦操作完成,系统就会将对象变成触发状态,这样线程就知道操作完成了。然后线程就可以继续执行。
线程可以调用WaitForInputIdle函数来将自己挂起。该函数原型为:
DWORD WaitForInputIdle(HANDLE hProcess,DWORD dwMilliseconds);
这个函数会等待由hProcess标识的进程,直到创建应用程序第一个窗口的线程中没有待处理的输入为止。应用场景举例:父进程可以创建一个子进程来完成一些工作。当父进程调用CreateProcess的时候,父进程可以一边继续执行,一边让子进程进行初始化。父进程可能需要得到子进程创建的窗口句柄。父进程能够知道子进程已经初始化完毕的唯一方法就是等待子进程,直到它不再处理任何输入为止。因此可在调用CreateProcess之后,父进程调用WaitForInputIdle。
线程也可以调用MsgWaitForMultipleObjects(Ex)函数,这使得线程等待需要自己处理的消息。这类函数不仅内核对象被触发时,调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。
创建窗口的线程和执行与用户界面相关任务的线程不应该使用WaitForMultipleObjects,而应该使用MsgWaitForMultipleObjectsEx。因为前者会妨碍线程对用户在界面上的操作进行响应。
SignalObjectAndWait函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象。SignalObjectAndWait函数原型为:DWORD SignalObjectAndWait(
HANDLE hObjectToSignal,//必须是一个互斥量、信号量或事件对应的句柄
HANDLE hObjectToWaitOn,//可以标志的内核对象:互斥量、信号量、事件、计时器、进程、线程、作业、控制台输入以及变更通知
DWORD dwMilliseconds,//函数等待对象触发的最长时间
BOOL bAlertable//当线程处于等待状态时,是否应该能够对添加到队列中的异步过程调用进行处理
);
微软提供了一组新的等待链遍历(WCT)API,这些函数可以让我们列出所有的锁,并检测进程内部,甚至是进程之间的死锁。
WCF 所记录的同步机制的类型如下