swneng

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

文章原始出处 http://xxinside.blogbus.com/logs/47523285.html

预备知识:C#线程同步(1)- 临界区&Lock,C#线程同步(2)- 临界区&Monitor,C#线程同步(3)- 互斥量 Mutex

WaitHandle一家

  在前一篇我们已经提到过Mutex和本篇的主角们直接或间接继承自WaitHandle:

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 )

  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那篇提到的生产者、消费者和糖罐的场景。

  回过头来仔细查看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()来等待所有消费者吃完再进行生产。

posted on 2018-12-07 09:48  易哲  阅读(805)  评论(0编辑  收藏  举报