监视器(Monitor)的概念 可以在MSDN(http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx)上找到下面一段话: 与lock关键字类似,监视器防止多个线程同时执行代码块。Enter方法允许一个且仅一个线程继续执行后面的语句;其他所有线程都将被阻止,直到执行语句的线程调用Exit。这与使用lock关键字一样。事实上,lock 关键字就是用Monitor 类来实现的。例如: lock(x) { DoSomething(); } 这等效于: System.Object obj = (System.Object)x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); } 使用 lock 关键字通常比直接使用 Monitor 类更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 关键字来实现的,无论是否引发异常它都执行关联的代码块。 这里微软已经说得很清楚了,Lock就是用Monitor实现的,两者都是C#中对临界区功能的实现。用ILDASM打开含有以下代码的exe或者dll也可以证实这一点(我并没有自己证实): lock (lockobject) { int i = 5; } 反编译后的的IL代码为: IL_0045: call void [mscorlib]System.Threading.Monitor::Enter(object) IL_004a: nop .try { IL_004b: nop IL_004c: ldc.i4.5 IL_004d: stloc.1 IL_004e: nop IL_004f: leave.s IL_0059 } // end .try finally { IL_0051: ldloc.3 IL_0052: call void [mscorlib]System.Threading.Monitor::Exit(object) IL_0057: nop IL_0058: endfinally } // end handler Monitor中和lock等效的方法 Monitor是一个静态类,因此不能被实例化,只能直接调用Monitor上的各种方法来完成与lock相同的功能: - Enter(object)/TryEnter(object)/TryEnter(object, int32)/TryEnter(object, timespan):用来获取对象锁(Lock中已经提到过,这里再强调一次,是对象类型而不能是值类型),标记临界区的开始。与Enter不同,TryEnter永远不会阻塞代码,当无法获取对象锁时它会返回False,并且调用者不进入临界区。TryEnter还有两种重载,可以定义一个时间段,在该时间段内一直尝试获得对象锁,超时则返回False。
- Exit(object):没啥好说的,释放对象锁、退出临界区。只是一定记得在try的finally块里调用,否则一但由于异常造成Exit无法执行,对象锁得不到释放,就会造成死锁。此外,调用Exit的线程必须拥有 object 参数上的锁,否则会引发SynchronizationLockException异常。在调用线程获取指定对象上的锁后,可以重复对该对象进行了相同次数的 Exit 和 Enter 调用;如果调用 Exit 与调用 Enter 的次数不匹配,那么该锁不会被正确释放。
上篇中提到的有关lock的所有使用方法和建议,都适用于它们。
比lock更“高级”的Monitor 到此为止,所有见到的还是我们在lock中熟悉的东西,再看Monitor的其它方法之前,我们来看看那老掉牙的“生产者和消费者”场景。试想消费者和生产者是两个独立的线程,同时访问一个容器: - 很显然这个容器是一个临界资源(你不会问我为什么是显然吧?),同时只允许一个线程访问。
- 生产者往容器里存放生产好的资源;消费者消费掉容器里的资源。
粗看这个场景并没有什么特殊的问题,只要在两个线程中分别调用两个方法,这两个方法内部都用同一把锁进入临界区访问容器即可。可是问题在于: - 消费者锁定容器,进入临界区后可能发现容器是空的。它可以退出临界区,然后下次再盲目地进入碰碰运气;如果不退出,那么让生产者永远无法进入临界区,往容器里放入资源供消费者消费,从而造成死锁。
- 而生产者也可能进入临界区后,却发现容器是满的。结果一样,直接退出等下次来碰运气;或者不退出造成死锁。
两者选择直接退出不会引发什么问题,无非就是可能多次无功而返。这么做,你的程序逻辑总是有机会得到正确执行的,但是效率很低,因为这样的机制本身是不可控的,业务逻辑是否得以成功执行完全是随机的。 所以我们需要更有效、更“优雅”的方式: - 消费者在进入临界区发现容器为空后,立即释放锁并把自己阻塞,等待生产者通知,不再做无谓的尝试;如果顺利消费资源完毕后,主动通知生产者可以进行生产了,随后仍然阻塞自己等待生产者通知。
- 生产者如果发现容器是满的,那么立即释放锁并阻塞自己,等待消费者在消费完成后唤醒;在生产完毕后,主动给消费者发出通知,随后也仍然阻塞自己,等待消费者告诉自己容器已经空了。
在按这个思路写出Sample Code前,我们来看Monitor上需要用的其它重要方法: - Wait(Object)/Wait(Object, Int32)/Wait(Object, TimeSpan)/Wait(Object, Int32, Boolean)/Wait(Object, TimeSpan, Boolean): 释放对象上的锁并阻塞当前线程,直到它重新获取该锁。
- 这里的阻塞是指当前线程进入“WaitSleepJoin”状态,此时CPU不再会分配给这种状态的线程CPU时间片,这其实跟在线程上调用Sleep()时的状态一样。这时,线程不会参与对该锁的分配争夺。
- 要打破这种状态,需要其它拥有该对象锁的线程,调用下面要讲到的Pulse()来唤醒。不过这与,Sleep()不同,只有那些因为该对象锁阻塞的线程才会被唤醒。此时,线程重新进入“Running”状态,参与对对象锁的争夺。
- 强调一下,Wait()其实起到了Exit()的作用,也就是释放当前所获得的对象锁。只不过Wait()同时又阻塞了自己。
- 我们还看到Wait()的几个重载方法。其中第2、3个方法给Wait加上了一个时间,如果超时Wait会返回不再阻塞,并且可以根据Wait 方法的返回值,以确定它是否已在超时前重新获取锁。在这种情况下,其实线程并不需要等待其它线程Pulse()唤醒,相当于Sleep一定时间后醒来。第4、5个方法在第2、3个方法的基础上加上exitContent参数,我们暂时不去管它,你可以详细参见这里:http://msdn.microsoft.com/zh-cn/library/79fkfcw1(VS.85).aspx。
- Pulse(object):向阻塞线程队列(由于该object而转入WaitSleepJoin状态的所有线程,也就是那些执行了Wait(object)的线程,存放的队列)中第一个线程发信号,该信号通知锁定对象的状态已更改,并且锁的所有者准备释放该锁。收到信号的阻塞线程进入就绪队列中(那些处于Running状态的线程,可以被CPU调用运行的线程在这个队列里),以便它有机会接收对象锁。注意,接受到信号的线程只会从阻塞中被唤醒,并不一定会获得对象锁。
- PulseAll(object):与Pulse()不同,阻塞队列中的所有线程都会收到信号,并被唤醒转入Running状态,即进入就绪队列中。至于它们谁会幸运的获得对象锁,那就要看CPU了。
- 注意:以上所有方法都只能在临界区内被调用,换句话说,只有对象锁的获得者能够正确调用它们,否则会引发SynchronizationLockException异常。
好了,有了它们我们就可以完成这样的代码: using System; using System.Threading; using System.Collections; using System.Linq; using System.Text; class MonitorSample { //容器,一个只能容纳一块糖的糖盒子。PS:现在MS已经不推荐使用ArrayList, //支持泛型的List才是应该在程序中使用的,我这里偷懒,不想再去写一个Candy类了。 private ArrayList _candyBox = new ArrayList(1); private volatile bool _shouldStop = false; //用于控制线程正常结束的标志 /// <summary> /// 用于结束Produce()和Consume()在辅助线程中的执行 /// </summary> public void StopThread() { _shouldStop = true; //这时候生产者/消费者之一可能因为在阻塞中而没有机会看到结束标志, //而另一个线程顺利结束,所以剩下的那个一定长眠不醒,需要我们在这里尝试叫醒它们。 //不过这并不能确保线程能顺利结束,因为可能我们刚刚发送信号以后,线程才阻塞自己。 Monitor.Enter(_candyBox); try { Monitor.PulseAll(_candyBox); } finally { Monitor.Exit(_candyBox); } } /// <summary> /// 生产者的方法 /// </summary> public void Produce() { while(!_shouldStop) { Monitor.Enter(_candyBox); try { if (_candyBox.Count==0) { _candyBox.Add("A candy"); Console.WriteLine("生产者:有糖吃啦!"); //唤醒可能现在正在阻塞中的消费者 Monitor.Pulse(_candyBox); Console.WriteLine("生产者:赶快来吃!!"); //调用Wait方法释放对象上的锁,并使生产者线程状态转为WaitSleepJoin,阻止该线程被CPU调用(跟Sleep一样) //直到消费者线程调用Pulse(_candyBox)使该线程进入到Running状态 Monitor.Wait(_candyBox); |