文章原始出处 http://xxinside.blogbus.com/logs/47523285.htmlhtml
预备知识:C#线程同步(1)- 临界区&Lock,C#线程同步(2)- 临界区&Monitor,C#线程同步(3)- 互斥量 Mutex数组
WaitHandle一家安全
在前一篇咱们已经提到过Mutex和本篇的主角们直接或间接继承自WaitHandle:ide
Mutex类,这个咱们在上一篇已经讲过。
EventWaitHandle 类及其派生类AutoResetEvent 和 ManualResetEvent,这是本篇的主角。
Semaphore 类,即信号量,咱们下一篇再讲。
WaitHandle提供了若干用于同步的方法。上一篇关于Mutex的blog中已经讲到一个WaitOne(),这是一个实例方法。除此以外,WaitHandle另有3个用于同步的静态方法:函数
SignalAndWait(WaitHandle, WaitHandle):以原子操做的形式,向第一个WaitHandle发出信号并等待第二个。即唤醒阻塞在第一个WaitHandle上的线程/进程,而后本身等待第二个WaitHandle,且这两个动做是原子性的。跟WaitOne()同样,这个方法另有两个重载方法,分别用Int32或者TimeSpan来定义等待超时时间,以及是否从上下文的同步域中退出。
WaitAll(WaitHandle[]):这是用于等待WaitHandle数组里的全部成员。若是一项工做,须要等待前面全部人完成才能继续,那么这个方法就是一个很好的选择。仍然有两个用于控制等待超时的重载方法,请自行参阅。
WaitAny(WaitHandle[]):与WaitAll()不一样,WaitAny只要等到数组中一个成员收到信号就会返回。若是一项工做,你只要等最快作完的那个完成就能够开始,那么WaitAny()就是你所须要的。它一样有两个用于控制等待超时的重载。
线程相关性(Thread Affinity ).net
EventWaitHandle和Mutex二者虽然是派生自同一父类,但有着彻底不一样的线程相关性:线程
Mutex与Monitor同样,是“线程相关(Thread Affinity)”的。咱们以前已经提到过,只有经过Monitor.Enter()/TryEnter()得到对象锁的线程才能调用Pulse()/Wait()/Exit();一样的,只有得到Mutex拥有权的线程才能执行ReleaseMutex()方法,不然就会引起异常。这就是所谓的线程相关性。
相反,EventWaitHandle以及它的派生类AutoResetEvent和ManualResetEvent都是线程无关的。任何线程均可以发信号给EventWaitHandle,以唤醒阻塞在上面的线程。
下一篇要提到的Semaphore也是线程无关的。
Mutex与Event设计
咱们在Mutex一篇中没有具体提到Mutex是否能发送信号,只是简单说Mutex不太适合有相互消息通知的同步,它仅有的一些同步方法是来自其父类的静态方法。那么如今咱们能够仔细来看看Mutex到底能不能用于关于Monitor那篇提到的生产者、消费者和糖罐的场景。htm
回过头来仔细查看Mutex的全部方法,除了一个咱们已经提到的WaitHandle上的静态方法SingnalAndWait(toSingnal, toWaitOn),咱们找不到任何“属于Mutex本身”的、用于发送信号的方法。退而求其次吧,咱们就来看看这个静态方法是否可让Mutex具备通知的能力。对象
若是toSignal是一个Mutex,那么收到“信号”就等效于ReleaseMutex()。而因为Mutex的线程相关性,只有拥有当前Mutex的线程才可以发送这个信号(ReleaseMutex),不然会引起异常。也就是说若是要用这个方法来通知其它线程同步,Mutex只能本身发给本身。与之相反,若是第二个参数toWaitOn也是个Mutex,那么这个Mutex不能是本身。由于前篇已经讲过,Mutex的拥有者能够屡次WaitOne()而不阻塞,这里也是同样。因此若是Mutex必定要使用这个方法,准确的说是只是成为这个方法的参数,那只能是WaitHandle.SignalAndWait(它本身,另外一个Mutex)。
试想,若是有人试图只使用Mutex来进行同步通知。假设生产者线程经过Mutex上的WaitOne()得到了mutexA的拥有权,而且在生产完毕后调用了SingnalAndWait(mutexA,mutexB),通知因为当前mutexA而阻塞的消费者线程,而且将本身阻塞在mutexB上。那么被唤醒的消费者线程得到MutexA的拥有权吃掉糖后,也只能调用SingnalAndWait(mutexA,mutexB)释放它得到的mutexA且阻塞于MutexB。问题来了,此时的生产者是阻塞在mutexB上……也许,咱们能够设计一段“精巧”的代码,让生产者和消费者一下子阻塞在mutexA,一下子阻塞在mutexB上……我不想花费这个力气去想了,你能够试试看:)。无论有没有这样的可能,Mutex很明显就不适用于通知的场景。
EventWaitHandle的独门秘笈
正由于Mutex没有很好地继承父辈的衣钵,EventWaitHandle以及它的儿子/女儿们便来到了这个世界上。
EventWaitHandle、AutoResetEvent、ManualResetEvent名字里都有一个“Event”,不过这跟.net的自己的事件机制彻底没有关系,它不涉及任何委托或事件处理程序。相对于咱们以前碰到的Monitor和Mutex须要线程去争夺“锁”而言,咱们能够把它们理解为一些须要线程等待的“事件”。线程经过等待这些事件的“发生”,把本身阻塞起来。一旦“事件”完成,被阻塞的线程在收到信号后就能够继续工做。
为了配合WaitHandle上的3个静态方法SingnalAndWait()/WailAny()/WaitAll(),EventWaitHandle提供了本身独有的,使“Event”完成和从新开始的方法:
bool:Set():英文版MSDN:Sets the state of the event to signaled, allowing one or more waiting threads to proceed;中文版MSDN:将事件状态设置为终止状态,容许一个或多个等待线程继续。初看“signaled”和“终止”彷佛并不对应,细想起来这二者的说法其实也不矛盾。事件若是在进行中,固然就没有“终止”,那么其它线程就须要等待;一旦事件完成,那么事件就“终止”了,因而咱们发送信号唤醒等待的线程,因此“信号已发送”状态也是合理的。两个小细节:
不管中文仍是英文版,都提到这个方法都是可让“一个”或“多个”等待线程“继续/Proceed”(注意不是“唤醒”)。因此这个方法在“唤醒”这个动做上是相似于Monitor.Pulse()和Monitor.PulseAll()的。至于何时相似Pulse(),又在何时相似PulseAll(),往下看。
这个方法有bool型的返回值:若是该操做成功,则为true;不然,为false。不过MSDN并无告诉咱们,何时执行会失败,你只有找个微软MVP问问了。
bool:Reset():Sets the state of the event to nonsignaled, causing threads to block. 将事件状态设置为非终止状态,致使线程阻止。 一样,咱们须要明白“nonsignaled”和“非终止”是一回事情。还一样的是,仍然有个无厘头的返回值。Reset()的做用,至关于让事件从新开始处于“进行中”,那么此后全部WaitOne()/WaitAll()/WaitAny()/SignalAndWait()这个事件的线程都会再次被挡在门外。
来看看EventWaitHandle众多构造函数中最简单的一个:
EventWaitHandle(Boolean initialState, EventResetMode mode):初始化EventWaitHandle类的新实例,并指定等待句柄最初是否处于终止状态,以及它是自动重置仍是手动重置。大多数时候咱们会在第一个参数里使用false,这样新实例会缺省为“非终止”状态。第二个参数EventResetMode是一个枚举,一共两个值:
EventResetMode.AutoReset:当Set()被调用当前EventWaitHandle转入终止状态时,如有线程阻塞在当前EventWaitHandle上,那么在释放一个线程后EventWaitHandle就会自动重置(至关于自动调用Reset())再次转入非终止状态,剩余的原来阻塞的线程(若是有的话)还会继续阻塞。若是调用Set()后本没有线程阻塞,那么EventWaitHandle将保持“终止”状态直到一个线程尝试等待该事件,这个该线程不会被阻塞,此后EventWaitHandle才会自动重置并阻塞那以后的全部线程。
EventResetMode.ManualReset:当终止时,EventWaitHandle 释放全部等待的线程,并在手动重置前,即Reset()被调用前,一直保持终止状态。
好了,如今咱们能够清楚的知道Set()在何时分别相似于Monitor.Pulse()/PulseAll()了:
当EventWaitHandle工做在AutoReset模式下,就唤醒功能而言,Set()与Monitor.Pulse()相似。此时,Set()只能唤醒众多(若是有多个的话)被阻塞线程中的一个。但二者仍有些差异:
Set()的做用不只仅是“唤醒”而是“释放”,可让线程继续工做(proceed);相反,Pulse()唤醒的线程只是从新进入Running状态,参与对象锁的争夺,谁都不能保证它必定会得到对象锁。
Pulse()的已被调用的状态不会被维护。所以,若是在没有等待线程时调用Pulse(),那么下一个调用Monitor.Wait()的线程仍然会被阻塞,就像Pulse() 没有被被调用过。也就是说Monitor.Pulse()只在调用当时发挥做用,并不象Set()的做用会持续到下一个WaitXXX()。
在一个工做在ManualReset模式下的EventWaitHandle的Set()方法被调用时,它所起到的唤醒做用与Monitor.PulseAll()相似,全部被阻塞的线程都会收到信号被唤醒。而二者的差异与上面彻底相同。
来看看EventWaitHandle的其它构造函数:
EventWaitHandle(Boolean initialState, EventResetMode mode, String name):头两个参数咱们已经看过,第三个参数name用于在系统范围内指定同步事件的名称。是的,正如咱们在Mutex一篇中提到的,因为父类WaitHandle是具备跨进程域的能力的,所以跟Mutex同样,咱们能够建立一个全局的EventWaitHandle,让后将它用于进程间的通知。注意,name仍然是大小写敏感的,仍然有命名前缀的问题跟,你能够参照这里。当name为null或空字符串时,这等效于建立一个局部的未命名的EventWaitHandle。仍然一样的还有,可能会由于已经系统中已经有同名的EventWaitHandle而仅仅返回一个实例表示同名的EventWaitHandle。因此最后仍旧一样地,若是你须要知道这个EventWaitHandle是否由你最早建立,你须要使用如下两个构造函数之一。
EventWaitHandle(Boolean initialState, EventResetMode mode, String name, out Boolean createdNew):createdNew用于代表是否成功建立了EventWaitHandle,true代表成功,false代表已经存在同名的事件。
EventWaitHandle(Boolean initialState, EventResetMode mode, String name, out Boolean createdNew, EventWaitHandleSecurity):关于安全的问题,直接查看这个构造函数上的例子吧。全局MutexEventWaitHandle的安全问题应该相对Mutex更须要注意,由于有可能黑客程序用相同的事件名对你的线程发送信号或者进行组织,那样可能会严重危害你的业务逻辑。
好啦,都差很少了,能够写一个例子试试了。让咱们回到Monitor一篇中提到的生产者和消费者场景,让咱们看看EventWaitHandle能不能完成它兄弟Mutex没有能完成的事业。不过,即使有强大通讯能力的EventWaitHandle出马,也避免不要使用lock/monitor或是Mutex。缘由很简单,糖罐是一个互斥资源,必须被互斥地访问。而EventWaitHanldle跟Mutex相反,能通讯了但却彻底失去了临界区的能力。因此,这个例子其实并不太适合展现EventWaitHandle的通讯机制,我只是为了想用一样的例子来比较这些同步机制间的差别。
EventWaitHandle虽然还必须借助lock/Monitor/Mutex来实现这个例子(仅仅是临界区部分),可是它终究有强于Monitor的通讯能力,因此让咱们来扩展一下这个例子:如今有一个生产者,有多个消费者。
咱们让消费者在没有糖吃或吃完一块糖后阻塞在一个工做在ManualReset模式下的EventWaitHandle,生产者在生产完毕后就经过这个事件唤醒全部消费者吃糖。因为咱们使用了lock的关系,虽然全部消费者都被唤醒,可是他们仍是由于争夺糖罐的关系只有一个能进入临界区吃糖。不过此时阻塞的缘由并非由于咱们的通知时间,而是临界区的问题。 每一个消费者有一条专线,即一个工做在AutoRest模式下的EventWaitHandle,用于在吃完糖后通知生产者。而生产者用WaitAny()来等待消费者吃糖时间的发生,只要有任一消费者吃完糖,那么生产者就试图争夺对糖罐的拥有权,把糖罐塞满(一人一颗的标准)。消费者这里使用了WaitAndSignal给生产者发消息,并等待生产者进入临界区生产糖后通知他们。在这样的设计逻辑下,可能糖罐中的糖尚未所有吃完生产者就有机会再次把糖罐装满。固然,你也可使用了WaitAll()来等待全部消费者吃完再进行生产。