.Net CLR 中的同步机制(二): 信号量Semaphore

信号量Semaphore是另外一个CLR中的内核同步对象。在.net中,类Semaphore封装了这个对象。与标准的排他锁对象(Monitor,Mutex,SpinLock)不同的是,它不是一个排他的锁对象,它与SemaphoreSlim,ReaderWriteLock等一样允许多个有限的线程同时访问共享内存资源。

Semaphore就好像一个栅栏,有一定的容量,当里面的线程数量到达设置的最大值时候,就没有线程可以进去。然后,如果一个线程工作完成以后出来了,那下一个线程就可以进去了。Semaphore的WaitOne或Release等操作分别将自动地递减或者递增信号量的当前计数值。当线程试图对计数值已经为0的信号量执行WaitOne操作时,线程将阻塞直到计数值大于0。在构造Semaphore时,最少需要2个参数。信号量的初始容量和最大的容量。

        static Semaphore s1 = new Semaphore(1,2);
        
        static void Main(string[] args)
        {
            Task.Factory.StartNew(() => DoWork());
            Task.Factory.StartNew(() => DoWork());
 
            Console.ReadLine();
        }
 
        static void DoWork()
        {
            try
            {
                Console.WriteLine(string.Format("Thread {0} try to do work.", Thread.CurrentThread.ManagedThreadId));
                s1.WaitOne();
                Console.WriteLine(string.Format("Thread {0} is doing work.", Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(5000);
                Console.WriteLine(string.Format("Thread {0} is finising work.", Thread.CurrentThread.ManagedThreadId));
            }
            finally
            {
                s1.Release();
            }
        }

上面例子中,线程池中第二个执行的线程会被阻塞5秒钟。因为构造函数中,初始容量为1,只能允许一个线程经过。

最大容量为1的Semaphore,就类似于排他的锁对象,Monitor和Mutex。只是Semaphore是一种没有所有者的锁对象。我们在使用Mutex和Monitor的时候,只有该对象的所有者(成功获取该锁对象)才能释放锁对象,而Semaphore则不然,任何线程可以成功调用Semaphore的Release方法。

通常我们可以需要在保护有限的资源上面使用信号量,比如说控制数据库连接池的连接数,限制共享内存访问的线程数等等。需要注意的是,当最大容量大于1的时候,我们并不能用来保证数据同步性,因为有多个线程可以同时进入临界区。而容量为1的则可以保证。

下面是一个阻塞有界队列的数据结构,可以用在生产者和消费者模式中。

    public class BlockingBoundedQueue<T>
    {
        private Queue<T> _queue = new Queue<T>();
        private Mutex _mutex = new Mutex();
        private Semaphore _producerSemaphore;
        private Semaphore _consumerSemaphore;
 
        public BlockingBoundedQueue(int maxProducerCount, int maxConsumerCount)
        {
            _producerSemaphore = new Semaphore(maxProducerCount, maxProducerCount);
            _consumerSemaphore = new Semaphore(0, maxConsumerCount);
        }
 
        public void Enqueue(T obj)
        {
            //如果生产者到上限或队列满了,则阻塞,等待消费者取走一个可用项。
            _producerSemaphore.WaitOne();
 
            _mutex.WaitOne();
 
            try
            {
                _queue.Enqueue(obj);
            }
            finally 
            {
                _mutex.ReleaseMutex();
            }
            //有一个可用项,唤醒一个消费者。
            _consumerSemaphore.Release();
        }
 
        public T Dequeue()
        {
            //如果消费者到上限或队列为空,则阻塞,等待消费者取走一个可用项。
            _consumerSemaphore.WaitOne();
 
            T obj = default(T);
 
            _mutex.WaitOne();
 
            try
            {
                obj = _queue.Dequeue();
            }
            finally
            {
                _mutex.ReleaseMutex();
            }
            //取走一个可用项,唤醒一个生产者。
            _producerSemaphore.Release();
 
            return obj;
        }
    }

该示例使用了Semaphore和上一篇说到的Mutex,我们用Mutex来实现数据同步,使用Semaphore来实现控制同步。由于都使用了内核对象,所以该数据结构的效率并不高。

和Mutex类似,Semaphore可以用来控制进程级别的同步,由于Semaphore的特点,我们可以控制0-N个进程同时运行。

        static void TestProcessCount()
        {
            bool reatedNew = false;
            //控制运行2个进程
            using (var s = new Semaphore(2, 2, "Global\\Demo", out reatedNew))
            {
                if (!s.WaitOne(TimeSpan.FromSeconds(5), true))
                {
                    Console.WriteLine("Another app instance is running. Bye!");
                    return;
                }
                Console.WriteLine("Runing...");
                Console.ReadLine();
                s.Release();
            }
        }

Semaphore的WaitOne或者Release方法的调用大约会耗费1微秒的系统时间,而优化后的SemaphoreSlim则需要大致四分之一微秒。在计算中大量频繁使用它的时候SemaphoreSlim还是优势明显,加上SemaphoreSlim还丰富了不少接口,更加方便我们进行控制,所以在4.0以后的多线程开发中,推荐使用SemaphoreSlim。

测试代码从这里下载

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