• .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

    测试代码在这里下载

  • 相关阅读:
    preg_replace函数/e后门
    php7.0-7.3的bypass disable_function一把梭
    PHP反序列化逃逸
    day2filter_var函数漏洞
    基于 Elasticsearch 聚合搜索实现电商筛选查询功能
    基于SpringBoot + Redis + 轮询实现扫码登录
    教你理解Lambda表达式
    记录解决 Elasticseach 过滤与聚合问题
    基于 MyBatis-Plus 解决数据库逻辑删除与唯一索引问题
    Java8 Stream Lamdba sorted()排序遇到的小坑
  • 原文地址:https://www.cnblogs.com/haoxinyue/p/2918813.html
Copyright © 2020-2023  润新知