信号量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。
测试代码从这里下载