.Net CLR 中的同步机制(三): AutoResetEvent和ManualResetEvent

这里所说的事件是最基本的控制同步原语,不同于.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.");
        }

结果:

N`[T33XBCFC3HF}96{CG21T

Barrier也有类似的功能,但是它不像CountdownEvent,CountdownEvent满足条件之后就一直执行下去了,但Barrier有SignalAndWait,信号以后还继续等待。有一种“步骤”的感觉。因为一张图:

LS8136@DJD$1PT`KADDFE`Y

下面一个例子就是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();
            }
        }

N@763NE73{PKE6AN78[45{J

测试代码在这里下载

原文地址:https://www.cnblogs.com/haoxinyue/p/2918813.html