您的位置:首页 > 编程语言

《Windows核心编程》——九 用内核对象进行线程同步

2015-01-08 17:18 411 查看
前言:

在用户模式下进行线程同步的最大好处就是速度非常快。与用户模式下的同步机制相比,内核对象的用途要广泛的多,内核对象唯一的缺点就是它们的性能。调用本章介绍的任何一个新函数时,调用线程必须从用户模式切换到内核模式。对于线程同步来说,这些内核对象中的每一种要么处于触发状态,要么处于未触发状态。比如进程创建时,进程内核对象为未触发状态;进程结束时,进程内核对象为触发状态,当进程内核对象被触发后,它将永远保持这种状态,再也不会回到未触发状态。线程内核对象同进程内核对象。

下面的内核对象既可以处于触发状态,也可以处于未触发状态。如进程、线程、作业、文件以及控制台的标准输入流/输出流/错误流、事件、可等待的计时器、信号量、互斥量。线程可以自己切换到等待状态,直到另一个对象被触发为止。用来决定每个对象处于触发状态还是未触发状态的规则与对象的类型有关。我们可以把每个内核对象想象为都包含一面旗帜,当对象被触发的时候,旗帜升起;当对象为被触发的时候,旗帜落下。当线程正在等待的对象处于未触发状态(旗帜落下)的时候,它们是不可调度的。但是一旦对象被触发(旗帜升起),那么线程就会看到这面旗帜,从而变成可调度状态,然后很快就会继续执行。

9.1 等待函数

等待函数使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。注意,如果线程在调用一个等待函数的时候,相应的内核对象已经处于触发状态,那么线程是不会进入等待状态的。等待函数不会浪费宝贵的CPU时间,等待函数中最常用的是WaitForSingleObject,该函数的返回值表示为什么调用线程又能继续执行了。WaitForMultipleObjects允许调用线程同时检查多个内核对象的触发状态。

9.2 等待成功所引起的副作用

对一些内核对象来说,成功调用WaitForSingleObject或WaitForMultipleObjects事实上会改变对象的状态。如果对象的状态发生了改变,则称之为等待成功所引起的副作用。如现在假设线程正在等待一个自动重置事件对象,当事件对象被触发的时候,上述函数会检测到这一情况,这时它可以直接返回WAIT_OBJECT_0给调用线程。但是就在函数返回之前,上述函数会使事件变为非触发状态——这就是等待成功所引起的副作用。

WaitForMultipleObjects之所以这么有用的原因,是因为它能够以原子方式执行所有的操作。当线程调用WaitForMultipleObjects的时候,函数会测试所有对象的触发状态,并引发相应的副作用,所以这些都是作为一个操作来完成的。当该函数检查内核对象的状态时,任何其他线程都不能在背后修改对象的状态。

如果多个线程等待同一个内核对象,那么当对象被触发的时候,系统如何决定应该唤醒哪个线程?可能会唤醒任何一个,不论优先级高低、等待时间长短,对每个线程来说都有机会被唤醒,且机会是一样的,微软的解释是“算法是公平的”,实际上微软所使用的算法只不过是总所周知的“先入先出”机制。

9.3 事件内核对象(Event)



态。第二个线程知道第一个线程已经完成了它的工作。

自动重置事件可以想象为自动门,触发后(打开),只允许一个正在等待该事件的线程会变成可调度状态(只允许一个人进入,进入后就门就会被关闭);

手动重置事件可以想象为手动门,触发后(打开),正在等待该事件的所有线程都将变成可调度状态(允许所有人进入,直到门关闭)

CreateEvent总是被授予全部权限。但CreateEventEx更有用的地方在于它允许我们减少权限的方式来打开一个已经存在的时间,而CreateEvent总是要求全部权限。

一旦创建了事件,我们就可以直接控制它的状态。当调用SetEvent的时候,我们把事件变成触发状态;当调用ResetEvent的时候,我们把事件变成未触发状态。

微软为自动重置事件定义了一个等待成功所引起的副作用:当线程成功等待自动重置事件对象的时候,对象会自动地重置为未触发状态。对自动重置对象来说,通常不需要调用ResetEvent,这是因为系统会自动将事件重置。

最后总结下事件Event

1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件 ,另一个布尔值用来表示事件有无触发。

2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

3.事件可以解决线程间同步问题,因此也能解决互斥问题。

9.4 可等待的计时器内核对象(WaitableTimer)

可等待的计时器是这样一种内核对象,它们会在某个指定的时间触发,或每隔一段时间触发一次。它们通常用来在某个时间执行一些操作。和事件内核对象一样有手动重置和自动重置,方式也是一样的。

可以通过CreateWaitableTimer来创建一个可等待的计时器内核对象,在创建的时候可等待的计时器总是处于未触发状态。我们必须调用SetWaitableTimer来触发它的状态。每次调用SetWaitableTimer都会在设置新的触发时间之前将原来的触发时间取消。CancelWaitableTimer会把句柄标识的计时器取消,这样计时器就永远不会再触发了,除非以后再调用SetWaitableTimer来对它进行重置。

9.4.1 让可等待的计时器添加APC(异步过程调用)调用

当计时器触发的时候,微软还允许计时器把一个异步过程调用(APC)放到SetWaitableTimer的调用线程的队列中。

9.5 信号量内核对象(Semaphore)

信号量内核对象用来对资源进行计数。与其他所有内核对象相同,它们也包含一个使用计数,另外还包括两个32位值:一个最大资源计数和一个当前资源计数。最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量。

信号量的规则如下:

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

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

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

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

可以通过CreateSemaphore来创建一个信号量内核对象,线程可以通过ReleaseSemaphore来递增信号量的当前资源计数。



注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。

9.6 互斥量内核对象(Mutex)

互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量包含了一个使用计数、线程ID和一个递归计数。互斥量和关键段的行为完全相同。但是互斥量是内核对象,而关键段是用户模式下的同步对象。线程ID用来标识当前占用这个互斥量的是系统中的哪个线程,递归计数表示这个线程占用该互斥量的次数。互斥量有很多用途,它们是使用最为频繁的内核对象之一。它们一般用来对多个线程访问的同一块内存进行保护。互斥量可以确保正在访问内存块的任何线程独占对内存块的访问权,这样就维护了数据的完整性。互斥量只能用于互斥,不能用于同步。







9.6.1 遗弃问题(线程所有权)

互斥量与所有其他内核对象不同,这是因为它们具有“线程所有权”的概念。互斥量会记住自己是哪个线程等待成功的,这使它即使在未触发的状态下,也能为线程所获得。这个例外不仅适用于试图获得互斥量的线程,而且适用于试图释放互斥量的线程。当线程调用ReleaseMutex的时候,函数会检查调用线程ID与互斥量内部保存的线程ID是否一致。如果线程ID一致,递归计数会递减。如果线程ID不一致,那么ReleaseMutex将不执行任何操作并返回false给调用者。



最后总结下互斥量Mutex:

1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。

2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

9.8 其他的线程同步函数

9.8.1 异步设备I/O

当系统执行异步I/O操作的时候,设备对象处于未触发状态。一旦操作完成,系统会将对象变成触发状态。这样线程就知道操作已经完成了。这时,线程就可以继续执行。

9.8.2 WaitForInputIdle函数

线程可以调用这个函数来将自己挂起。比较实用的场景是:应用程序想要发送信息给一个窗口,但却不知道窗口什么时候能够创建完毕并就绪。该函数可以解决上述问题。

9.8.3 MsgWaitForMultipleObjects(Ex)函数

线程也可以调用该类函数,这使得线程等待需要自己处理的消息。这类函数与WaitForMultipleObjects类似。不同之处在于,不仅内核对象被触发的时候调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。创建窗口的线程和执行与用户界面相关的任务的线程不应该使用WaitForMultipleObjects,而应该使用MsgWaitForMultipleObjects(Ex)。这是因为前者会妨碍线程对用户在用户界面上的操作进行响应。

9.8.4 WaitForDebugEvent函数

9.8.5 SignalObjectAndWait函数

该函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象。触发的内核对象只能为以下几种:互斥量、信号量或事件。等待的内核对象可以是内核对象的任意一种。这个函数受欢迎的几个原因:首先,因为我们经常需要触发一个内核对象并等待另一个对象,让一个函数完成两个操作可以节省处理时间;其次,如果没有SignalObjectAndWait函数,那么一个线程就无法知道另一线程何时处于等待状态;

9.8.6 使用等待链遍历API来检测死锁
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: