这里所说的事件是最基本的控制同步原语,不同于.Net语言中的事件。在任何时刻,一个事件可能处于两种状态之一:已触发或者未触发,如果一个线程在一个未触发的事件上面等待,那么只有当这个事件的状态变成已触发时,这个线程才能继续执行;如果在等待时,事件已经处于已触发状态,那么线程将立即继续执行。
Windows提供了两种特殊的事件对象类型来实现线程之间的合作:自动设置事件和手动设置事件。他们都属于内核对象。这两种事件的差别是:当AutoResetEvent被触发时,只有一个线程可以看到这个信号,当线程看见这个信号时候,AutoResetEvent会自动切换到未触发状态。而ManualResetEvent需要手动调用方法来切换到未触发状态。如果有多个线程都在等待一个AutoResetEvent的触发状态,系统将会为这些等待的线程建立一个队列,当这个AutoResetEvent状态切换到触发状态的时候,只有一个线程可以看见这个状态的变化继续执行,其他的线程还必须要等到下一次状态切换到已触发。我们并不能保证先等待的线程会先继续执行,这里面涉及到内核线程调度的一些原因,比如优先级。 AutoResetEvent如果在没有线程等待的情况下,切换到已触发状态,那么以后第一个等待这个事件的线程将可以继续执行。然而对于ManualResetEvent, 所有等待的线程在ManualResetEvent设置成已触发状态的时候,都将继续执行。
一个简单的AutoResetEvent示例:
1 class Program 2 { 3 static AutoResetEvent are = new AutoResetEvent(false); 4 5 static void Main() 6 { 7 new Thread(Waiter).Start(); 8 Thread.Sleep(1000); 9 are.Set(); 10 11 Console.ReadLine(); 12 } 13 14 static void Waiter() 15 { 16 Console.WriteLine("Waiting..."); 17 are.WaitOne(); 18 Console.WriteLine("Notified"); 19 } 20 }
值得一提的是,AutoResetEvent的WaitOne方法,如果实参是0的话,则表示查看该AutoResetEvent的状态,不会阻塞操作。
下面是使用AutoResetEvent实现的BlockingQueue,使用AutoResetEvent的阻塞队列效率上要比Monitor和4.0的BlockingCollection差很多。
public class BlockingQueueWithEvent<T> { private Queue<T> _queue = new Queue<T>(); private Mutex _mutex = new Mutex(); private AutoResetEvent _event = new AutoResetEvent(false); public void Enqueue(T obj) { _mutex.WaitOne(); try { _queue.Enqueue(obj); } finally { _mutex.ReleaseMutex(); } //有一个可用项,唤醒一个消费者。 _event.Set(); } public T Dequeue() { T obj = default(T); bool taken = true; _mutex.WaitOne(); try { while (_queue.Count == 0) { taken = false; WaitHandle.SignalAndWait(_mutex, _event); _mutex.WaitOne(); taken = true; } obj = _queue.Dequeue(); } finally { if (taken) { _mutex.ReleaseMutex(); } } return obj; } }
代码中使用到了 WaitHandle.SignalAndWait(_mutex, _event) 方法。这是一个原子操作,表示给第一个参数_mutex一个信号,释放上面的锁。然后在第二个参数上面等待。
AutoResetEvent和ManualResetEvent这两种事件都没有所有者的概念,任何线程都可以切换事件的状态。同样,他们也没有递归性质,不像Mutex和Semaphore,内部有一个计数器。所以多次执行Set或Reset方法都没有任何其他的效果,当事件已经处于已触发状态时,多次调用Set实际上是被忽略。这个特性需要我们在开发程序中特别注意,往往这个唤醒(Set)会被遗失。比如说有两个生产者,前后分别向队列中放了一个项。而消费者在收到唤醒信号的时候只会去队列中拿走一个项。
这两个事件都会在拥有该事件的应用程序域销毁的时候自动销毁。
在 .NET Framework 4中,当等待时间预计非常短时,并且当事件不会跨越进程边界时,可使用 ManualResetEventSlim 类以获得更好的性能。因为它里面在某些地方使用了自旋,提高了性能。
在.NET Framework 4中,还提供了其他两个基于ManualResetEventSlim的新类型,CountdownEvent和ManualResetEventSlim,他们都是使用ManualResetEventSlim来实现的。
下面是CountdownEvent的示例,表示CountdownEvent需要收到3个事件信号才会继续执行:
static CountdownEvent cde = new CountdownEvent(3); static void TestCountDownEvent() { Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); cde.Signal(); }); Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); cde.Signal(); }); Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); cde.Signal(); }); cde.Wait(); Console.WriteLine("all are finished."); }
结果:
Barrier也有类似的功能,但是它不像CountdownEvent,CountdownEvent满足条件之后就一直执行下去了,但Barrier有SignalAndWait
,信号以后还继续等待。有一种“步骤”的感觉。因为一张图:
下面一个例子就是3个线程都打印0到4,5个数字。每个线程每打印一个数字,都需要停下来等待其他的线程完成这一轮打印,然后齐头并进打印下面一个数字。
static void Main() { TestBarrier(); Console.ReadLine(); } static Barrier b = new Barrier(3); static void TestBarrier() { Task.Factory.StartNew(TestBarrierMethod); Task.Factory.StartNew(TestBarrierMethod); Task.Factory.StartNew(TestBarrierMethod); } private static void TestBarrierMethod() { for (int i = 0; i < 5; i++) { Console.Write(i + " "); b.SignalAndWait(); } }
测试代码在这里下载