Monitor.Wait初探(1)
线程是一个操作系统的概念,线程的同步也涉及到操作系统的知识。但是具体到不同的开发环境和开发语言中,线程的同步各有各的特点。所以我们可能会感兴趣,这些各具特色的线程同步技术是否一定离不开操作系统底层提供的同步技术呢,还是会在实现的过程中加入一些自己的偏方和窍门。O,这个问题太大了,需要慢慢来讨论。
先解剖解剖一只小麻雀吧,这里我只对.Net 提供的Monitor类感兴趣,如有分析错误或不到位之处,还希望同学们积极交流反馈。
我们知道Managed Code C#中用来线程同步的技术有好几种,而最常用的可能就是加锁了,也即是使用关键词lock。
lock的语法如下:
lock (_lockObject)
{
//Put your protected codes here
}
我们知道lock只不过是C#提供的语法糖而已,lock只不过是对System.Threading命名空间下Monitor类的封装而已,一个lock block等价于:
try
{
Monitor.Enter(object);
//Put your protected codes here
}
finally
{
Monitor.Exit(object);
}
Monitor.Enter的MSDN描述是:在指定对象上获取排他锁。
使用 Enter 获取作为参数传递的对象上的 Monitor。 如果其他线程已对该对象执行了 Enter,但尚未执行对应的 Exit,则当前线程将阻止,直到对方线程释放该对象。 同一线程在不阻止的情况下多次调用 Enter 是合法的;但在该对象上等待的其他线程取消阻止之前必须调用相同数目的 Exit。
如果文章至此结束,我们至此只不过是温习了一种线程同步的技术。再看看文章的标题,没错,我要讨论的重点是Monitor.Wait方法,这又是Minitor类public的另一个和线程控制有关的操作,MSDN对Wait方法其中一个重载的描述如下:
public static bool Wait( Object obj )
释放对象上的锁并阻止当前线程,直到它重新获取该锁。
再进一步看其使用备注:
当前拥有指定对象上的锁的线程调用此方法以释放该对象,以便其他线程可以访问它。 调用方在等待重新获取锁期间被阻止。 当调用方需要等待另一个线程操作导致的状态更改时,将调用此方法。
当线程调用 Wait 时,它释放对象的锁并进入对象的等待队列。 对象的就绪队列中的下一个线程(如果有)获取锁并拥有对对象的独占使用。 所有调用 Wait 的线程都将留在等待队列中,直到它们接收到由锁的所有者发送的 Pulse 或 PulseAll 的信号为止。 如果发送了 Pulse,则只影响位于等待队列最前面的线程。 如果发送了PulseAll,则将影响正等待该对象的所有线程。 接收到信号后,一个或多个线程将离开等待队列而进入就绪队列。 就绪队列中的线程被允许重新获取锁。
读到这里我们明显懂了,Monitor.Wait是更细粒度的线程调度方式,我虽然通过Monitor.Enter锁定了对象,但是依然可以在lock块中调用Monitor.Wait从而释放当前线程对锁对象的拥有,从而心甘情愿把自己释放到等待队列中。
从描述中,我们还获得一个讯息,就是Wait必须通过Pulse或者PulseAll方法释放锁,否则Wait的线程会一直在等待队列出等待Pulse,从而使得该线程处于Hung状态。
现在一个问题来了,既然我可以通过Monitor.Enter和Exit所定义及同步线程了,为啥还需要Wait?原因很简单,提供更细粒度的线程调度。通过Wait函数我们可以在一个lock线程代码中等待另一个或多个线程完成某个计算之后(通过Pulse)再继续Wait线程Wait之后的代码执行。
好了,Wait的使用场景探讨就此打住,另一个问题来了:Wait做了什麽事情,为什麽能够让线程处于等待状态?
最好的方式就是亲自写一段代码调试一番。
代码非常简单,如下:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace ConsoleApplication1
{
class Program
{
private static object _lockObject = new object();
static void Main(string[] args)
{
Thread tr1 = new Thread(ThreadProc1);
Thread tr2 = new Thread(ThreadProc2);
tr1.Start();
tr2.Start();
tr1.Join();
tr2.Join();
Console.ReadKey();
}
static void ThreadProc1()
{
lock(_lockObject)
{
Monitor.Wait(_lockObject);
Console.WriteLine("1");
}
}
static void ThreadProc2()
{
lock (_lockObject)
{
Console.WriteLine("2");
}
}
}
}
示例代码启动了两个线程,通过_lockObject锁对象进行同步,通过分析程序逻辑我们大胆猜测代码的执行顺序应该是:
ThreadProc1线程先执行,ThreadProc2后启动,因为ThreadProc1先获得了锁对象,导致ThreadProc2会先等待锁对象的释放才能往下执行。
ThreadProc1进入之后第一行执行的代码就是Monitor.Wait,这又使得ThreadProc1所在的线程被流放到等待队列同时释放了锁对象的控制。于是,ThreadProc2自动获得锁对象控制权并开始执行代码Console.WriteLine(“2”).
该程序中我们注意到ThreadProc2并没有调用Pulse方法,故ThreadProc1会一直在Wait处等待,改程序的执行结果必然是有一个线程Hung住了。
不好意思,Hung就是我邪恶的目的,接下来就是挂载Windbg进行调试。