本随笔续接:
在前一篇我们已经提到过Mutex和本篇的主角们直接或间接继承自 :
- ,这个我们在上一篇已经讲过。
- 类及其派生类 和 ,这是本篇的主角。
- 类,即信号量,我们下一篇再讲(忽然觉得没有必要介绍了)。
WaitHandle提供了若干用于同步的方法。上一篇关于Mutex的blog中已经讲到一个WaitOne(),这是一个实例方法。除此之外,WaitHandle另有3个用于同步的静态方法:
- :以原子操作的形式,向第一个WaitHandle发出信号并等待第二个。即唤醒阻塞在第一个WaitHandle上的线程/进程,然后自己等待第二个WaitHandle,且这两个动作是原子性的。跟WaitOne()一样,这个方法另有两个重载方法,分别用Int32或者TimeSpan来定义等待超时时间,以及是否从上下文的同步域中退出。
- :这是用于等待WaitHandle数组里的所有成员。如果一项工作,需要等待前面所有人完成才能继续,那么这个方法就是一个很好的选择。仍然有两个用于控制等待超时的重载方法,请自行参阅。
- :与WaitAll()不同,WaitAny只要等到数组中一个成员收到信号就会返回。如果一项工作,你只要等最快做完的那个完成就可以开始,那么WaitAny()就是你所需要的。它同样有两个用于控制等待超时的重载。
线程相关性
- Mutex与Monitor一样,是具有线程相关性的。我们之前已经提到过,只有通过Monitor.Enter()/TryEnter()获得对象锁的线程才能调用Pulse()/Wait()/Exit();同样的,只有获得Mutex拥有权的线程才能执行ReleaseMutex()方法,否则就会引发异常。这就是所谓的线程相关性。
- 相反,EventWaitHandle以及它的派生类AutoResetEvent和ManualResetEvent都是线程无关的。任何线程都可以发信号给EventWaitHandle,以唤醒阻塞在上面的线程。
- 下一篇要提到的Semaphore也是线程无关的。
Event通知
EventWaitHandle、AutoResetEvent、ManualResetEvent名字里都有一个“Event”,不过这跟.net的本身的事件机制完全没有关系,它不涉及任何委托或事件处理程序。相对于我们之前碰到的Monitor和Mutex需要线程去争夺“锁”而言,我们可以把它们理解为一些需要线程等待的“事件”。线程通过等待这些事件的“发生”,把自己阻塞起来。一旦“事件”完成,被阻塞的线程在收到信号后就可以继续工作。
为了配合WaitHandle上的3个静态方法SingnalAndWait()/WailAny()/WaitAll(),EventWaitHandle提供了自己独有的,使“Event”完成和重新开始的方法:
-
- :英文版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问问了。
- :Sets the state of the event to nonsignaled, causing threads to block. 将事件状态设置为非终止状态,导致线程阻止。 同样,我们需要明白“nonsignaled”和“非终止”是一回事情。还同样的是,仍然有个无厘头的返回值。Reset()的作用,相当于让事件重新开始处于“进行中”,那么此后所有WaitOne()/WaitAll()/WaitAny()/SignalAndWait()这个事件的线程都会再次被挡在门外。
- :英文版MSDN:Sets the state of the event to signaled, allowing one or more waiting threads to proceed;中文版MSDN:将事件状态设置为终止状态,允许一个或多个等待线程继续。初看“signaled”和“终止”似乎并不对应,细想起来这两者的说法其实也不矛盾。事件如果在进行中,当然就没有“终止”,那么其它线程就需要等待;一旦事件完成,那么事件就“终止”了,于是我们发送信号唤醒等待的线程,所以“信号已发送”状态也是合理的。两个小细节:
构造函数
来看看EventWaitHandle众多构造函数中最简单的一个:
- :初始化EventWaitHandle类的新实例,并指定等待句柄最初是否处于终止状态,以及它是自动重置还是手动重置。大多数时候我们会在第一个参数里使用false,这样新实例会缺省为“非终止”状态。第二个参数是一个枚举,一共两个值:
- 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的其它构造函数:
- :头两个参数我们已经看过,第三个参数name用于在系统范围内指定同步事件的名称。是的,正如我们在Mutex一篇中提到的,由于父类WaitHandle是具有跨进程域的能力的,因此跟Mutex一样,我们可以创建一个全局的EventWaitHandle,让后将它用于进程间的通知。注意,name仍然是大小写敏感的,仍然有命名前缀的问题跟,你可以参照。当name为null或空字符串时,这等效于创建一个局部的未命名的EventWaitHandle。仍然同样的还有,可能会因为已经系统中已经有同名的EventWaitHandle而仅仅返回一个实例表示同名的EventWaitHandle。所以最后仍旧同样地,如果你需要知道这个EventWaitHandle是否由你最先创建,你需要使用以下两个构造函数之一。
- :createdNew用于表明是否成功创建了EventWaitHandle,true表明成功,false表明已经存在同名的事件。
- :关于安全的问题,直接查看这个构造函数上的例子吧。全局MutexEventWaitHandle的安全问题应该相对Mutex更需要注意,因为有可能黑客程序用相同的事件名对你的线程发送信号或者进行组织,那样可能会严重危害你的业务逻辑。
MSDN Demo
using System;using System.Threading;public class Example{ // The EventWaitHandle used to demonstrate the difference // between AutoReset and ManualReset synchronization events. // private static EventWaitHandle ewh; // A counter to make sure all threads are started and // blocked before any are released. A Long is used to show // the use of the 64-bit Interlocked methods. // private static long threadCount = 0; // An AutoReset event that allows the main thread to block // until an exiting thread has decremented the count. // private static EventWaitHandle clearCount = new EventWaitHandle(false, EventResetMode.AutoReset); [MTAThread] public static void Main() { // Create an AutoReset EventWaitHandle. // ewh = new EventWaitHandle(false, EventResetMode.AutoReset); // Create and start five numbered threads. Use the // ParameterizedThreadStart delegate, so the thread // number can be passed as an argument to the Start // method. for (int i = 0; i <= 4; i++) { Thread t = new Thread( new ParameterizedThreadStart(ThreadProc) ); t.Start(i); } // Wait until all the threads have started and blocked. // When multiple threads use a 64-bit value on a 32-bit // system, you must access the value through the // Interlocked class to guarantee thread safety. // while (Interlocked.Read(ref threadCount) < 5) { Thread.Sleep(500); } // Release one thread each time the user presses ENTER, // until all threads have been released. // while (Interlocked.Read(ref threadCount) > 0) { Console.WriteLine("Press ENTER to release a waiting thread."); Console.ReadLine(); // SignalAndWait signals the EventWaitHandle, which // releases exactly one thread before resetting, // because it was created with AutoReset mode. // SignalAndWait then blocks on clearCount, to // allow the signaled thread to decrement the count // before looping again. // WaitHandle.SignalAndWait(ewh, clearCount); } Console.WriteLine(); // Create a ManualReset EventWaitHandle. // ewh = new EventWaitHandle(false, EventResetMode.ManualReset); // Create and start five more numbered threads. // for(int i=0; i<=4; i++) { Thread t = new Thread( new ParameterizedThreadStart(ThreadProc) ); t.Start(i); } // Wait until all the threads have started and blocked. // while (Interlocked.Read(ref threadCount) < 5) { Thread.Sleep(500); } // Because the EventWaitHandle was created with // ManualReset mode, signaling it releases all the // waiting threads. // Console.WriteLine("Press ENTER to release the waiting threads."); Console.ReadLine(); ewh.Set(); } public static void ThreadProc(object data) { int index = (int) data; Console.WriteLine("Thread {0} blocks.", data); // Increment the count of blocked threads. Interlocked.Increment(ref threadCount); // Wait on the EventWaitHandle. ewh.WaitOne(); Console.WriteLine("Thread {0} exits.", data); // Decrement the count of blocked threads. Interlocked.Decrement(ref threadCount); // After signaling ewh, the main thread blocks on // clearCount until the signaled thread has // decremented the count. Signal it now. // clearCount.Set(); }}
附,Demo :
参见更多:
(该系列随笔暂告一段落)