本篇从 Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler 的类关系图开始,希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而对每种方式的使用细节,适用场合不会过多解释。

让我们来看看这几个类的关系图:
1. lock 关键字
    lock 是 C# 关键词,它将语句块标记为临界区,确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。方法是获取给定对象的互斥锁,执行语句,然后释放该锁。MSDN 上给出了使用 lock 时的注意事项通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。

    常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 。
        1)如果实例可以被公共访问,将出现 lock (this) 问题。
        2)如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。由于一个类的所有实例都只有一个类型对象(该对象是 typeof 的返回结果),锁定它,就锁定了该对象的所有实例。微软现在建议不要使用 lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。
        3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock("myLock") 问题。这个问题和.NET Framework 创建字符串的机制有关系,如果两个 string 变量值都是"myLock", 在内存中会指向同一字符串对象。最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。
    我们再来通过 IL Dasm 看看 lock 关键字的本质,下面是一段简单的测试代码:
        lock (lockobject)
        {
            int i = 5;
        }
    用 IL Dasm 打开编译后的文件,上面的语句块生成的 IL 代码为:
        IL_0045: call
        IL_004a: nop
        .try
        {
            IL_004b: nop
            void [mscorlib]System.Threading.Monitor::Enter(object)
            IL_004c: ldc.i4.5 IL_004d: stloc.1
        IL_004e: nop IL_004f: leave.s }
        // end .try finally { IL_0051: ldloc.3 IL_0052: call IL_0057: nop IL_0058: endfinally }
        // end handler
    通过上面的代码我们很清楚的看到:lock 关键字其实就是对 Monitor 类的 Enter()和 Exit()方法的封装。通过 try......catch......finally 语句块确保在 lock 语句块结束后执行 Monitor.Exit()方法,释放互斥锁。

 

2. Monitor 类
        Monitor类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问临界区的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。通过对 lock 关键字的分析我们知道,lock 就是对 Monitor 的 Enter 和 Exit 的一个封装,而且使用起来更简洁,因此 Monitor 类的 Enter()和 Exit()方法的组合使用可以用 lock 关键字替代。
   Monitor 类的常用方法:
        TryEnter():
            能够有效的解决长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用 TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间 bool gotLock = Monitor.TryEnter(myobject,1000),让当前线程在等待 1000 秒后根据返回的 bool 值来决定是否继续下面的操作。

        Wait() :

            释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。
        Pulse():
        PulseAll():
            向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。注意:Pulse、PulseAll 和 Wait 方法必须从同步的代码块内调用。

   

我们假定一种情景:妈妈做蛋糕,小孩有点馋,妈妈每做好一块就要吃掉,妈妈做好一块后,告诉小孩蛋糕已经做好了。下面的例子用 Monitor 类的 Wait 和 Pulse 方法模拟小孩吃蛋糕的情景。

using System;
using System.Threading;
 
/// <summary>
/// 仅仅是说明 Wait 和 Pulse/PulseAll 的例子
/// 逻辑上并不严密,使用场景也并不一定合适
/// </summary>
class MonitorSample
{
    private int n = 1; // 生产者和消费者共同处理的数据    
    private int max = 10000;    
    private object monitor = new object();
 
    public void Produce() // 生产
    {
        lock (monitor)
        {
            for (; n <= max; n++)
            {
                Console.WriteLine("妈妈:第" + n.ToString() + "块蛋糕做好了");
 
                // Pulse 方法不用调用是因为另一个线程中用的是 Wait(object,int) 方法 
                // 该方法使被阻止线程进入了同步对象的就绪队列 
                // 是否需要脉冲激活是 Wait 方法一个参数和两个参数的重要区别 
                Monitor.Pulse(monitor);
 
                // 调用 Wait 方法释放对象上的锁并阻止该线程(线程状态为 WaitSleepJoin) 
                // 该线程进入到同步对象的等待队列,直到其它线程调用 Pulse 使该线程进入到就绪队列中 
                // 线程进入到就绪队列中才有条件争夺同步对象的所有权 
                // 如果没有其它线程调用 Pulse/PulseAll 方法,该线程不可能被执行 
                Monitor.Wait(monitor);
            }
        }
    }
 
    public void Consume() // 消费
    {
        lock (monitor)
        {
            while (true)
            {            
                // 通知等待队列中的线程锁定对象状态的更改,但不会释放锁 
                // 接收到 Pulse 脉冲后,线程从同步对象的等待队列移动到就绪队列中 
                // 注意:最终能获得锁的线程并不一定是得到 Pulse 脉冲的线程 
                Monitor.Pulse(monitor);
 
                // 释放对象上的锁并阻止当前线程,直到它重新获取该锁 
                // 如果指定的超时间隔已过,则线程进入就绪队列 
                // 该方法只有在线程重新获得锁时才有返回值
                // 在超时等待时间内就获得了锁,返回结果为 true,否则为 false
                // 有时可以利用这个返回结果进行一个代码的分支处理
                Monitor.Wait(monitor, 1000);    
 
                Console.WriteLine("孩子:开始吃第" + n.ToString() + "块蛋糕");
            }
        }
    }
 
    static void Main(string[] args)
    {
        MonitorSample obj = new MonitorSample();
        Thread tProduce = new Thread(new ThreadStart(obj.Produce));
        Thread tConsume = new Thread(new ThreadStart(obj.Consume));
        tProduce.Start();
        tConsume.Start();
        Console.ReadLine();
    }
}
 

相关文章: