代码改变世界

C# 线程手册 第三章 使用线程 Monitor.Enter() 和 Monitor.Exit()

2012-02-03 17:03  DanielWise  阅读(5383)  评论(0编辑  收藏  举报

Monitor 方法是静态的,不需要生成Monitor 类的实例就可以直接调用它们。在.NET Framework 中,每个对象都有一个与之关联的锁,对象可以得到并释放它以便于在任意时间只有一个线程可以访问对象实例变量和方法。类似的,.NET Framework 中的每个对象都提供一个允许自己进入等待状态的机制。与锁的机制类似,这种机制的主要目的是为了实现线程间通信。当一个线程进入到一个对象的关键部分且需要一个特定条件并假设另外一个线程将会在同样的关键区域中创建条件时这种机制才会发生。

  现在比较特别的是在任意时间任意关键区域内仅允许有一个线程,而且当第一个线程进入到关键区域以后,其他线程都不能进入。现在问题来了,由于第一个线程已经独占了关键区域,那么第二个线程如何在关键区域内创建一个条件呢? 举个简单例子来说明如何解决这个问题的:如果线程A需要从数据库中获取一些数据,线程B等待所有数据被接收以后处理这些数据,线程B调用Wait()方法来等待线程A在数据到来时通知它。当数据到来以后,线程A会调用Pulse() 方法来通知线程B数据已经到达现在可以处理数据了。这是通过“等待&通知”机制实现的。第一个线程进入关键区域并执行Wait()方法。Wait()方法将锁释放后进入等待状态,然后第二个线程现在可以进入关键区域,改变相关条件,最后调用Pulse() 方法来通知等待线程条件已经满足,现在可以继续执行了。第一个线程再次获得锁,然后从Monitor.Wait() 方法返回并从Monitor.Wait()方法处继续执行。

任意两个线程都不可以同时进入Enter()方法。这与ATM 的例子类似:任意时间只允许一个人操作账户,其他人不可以操作直到第一个人离开。Monitor.Enter() 和 Monitor.Exit() 方法的命名是很贴近生活的。图2 描述了Monitor 类的工作机制。

2012-2-3 15-29-15

图2

 

现在我们来看一个使用Enter() 和 Exit() 方法的例子,MonitorEnterExit.cs:

/*************************************
/* Copyright (c) 2012 Daniel Dong
 * 
 * Author:oDaniel Dong
 * Blog:o  www.cnblogs.com/danielWise
 * Email:o guofoo@163.com
 * 
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace MonitorEnterExit
{
    public class EnterExit
    {
        private int result = 0;

        public void NonCriticalSection()
        {
            Console.WriteLine("Entered Thread "
                + Thread.CurrentThread.GetHashCode());

            for (int i = 1; i <= 5; i++)
            {
                Console.WriteLine("Result = " + result++
                    + " ThreadID " 
                    + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1000);
            }
            Console.WriteLine("Exiting Thread "
                + Thread.CurrentThread.GetHashCode());
        }

        public void CriticalSection()
        {
            //Enter the Critical Section
            Monitor.Enter(this);

            NonCriticalSection();

            //Exit the Critical Section
            Monitor.Exit(this);
        }

        public static void Main(string[] args)
        {
            EnterExit e = new EnterExit();

            if (args.Length > 0)
            {
                Thread nt1 =
                    new Thread(new ThreadStart(e.NonCriticalSection));
                nt1.Start();

                Thread nt2 =
                    new Thread(new ThreadStart(e.NonCriticalSection));
                nt2.Start();
            }
            else
            {
                Thread ct1 =
                    new Thread(new ThreadStart(e.CriticalSection));
                ct1.Start();

                Thread ct2 =
                    new Thread(new ThreadStart(e.CriticalSection));
                ct2.Start();
            }

            Console.ReadLine();
        }
    }
}

当你不输入任何参数时你将得到CriticalSection()方法的输出,结果如下:

2012-2-3 17-01-58

对应的,当你输入一个参数时,将会得到NonCriticalSection() 方法的相关输出:

2012-2-3 17-02-24

上面的例子中,我们定义了一个EnterExit 类、一个全局变量和两个方法:NonCriticalSection() 和 CriticalSection(). 在NonCriticalSection()部分我们没有使用任何监视器来锁住它,而在 CriticalSection()方法中使用监视器来锁住关键部分。这两个方法都修改结果值。

关键部分定义为Montor.Enter(this) 和 Monitor.Exit(this)之间的代码块。参数“this”表示锁应该按考虑设置在当前对象。把哪个对象作为Enter()方法的参数总是很难决定。当你需要锁住一个对象以便于其他线程不可以访问这个对象时,可以把相应对象的引用作为Enter()方法的参数。例如,在我们之前讨论的AccountWrapper例子中,我们把Account对象传递给Monitor, 而不是AccountWrapper对象的this指针。这是因为我们的目的是要锁住Account对象而不是AccountWrapper对象。我们不想让多个线程访问Account对象,但是不关注是否有多个对象访问AccountWrapper对象。

在Main()方法中,我们按照输入的参数运行不同方法。如果没有输入参数,就是用CriticalSection()方法,反之则是用NonCriticalSection()方法。在两种情况下,我们都有两个线程在同一时间访问方法并改变结果值。尽管他们是按顺序定义的,For循环以及睡眠时间将保证线程完成资源。

通过对比关键部分和非关键部分的输出可以让关键部分的概念更清晰。如果你观察NonCriticalSection()方法的输出,线程nt1 和 nt2 都在同一时间改变变量值,最终输出混乱结果。这是因为NonCriticalSection()方法没有锁,属于非线程安全的。多个线程可以访问这个方法并在同一时间获得全局变量值。另外一方面,如果你观察CriticalSection()方法的输出,结果很清晰的显示直到线程ct1退出关键部分,另外一个线程ct2才被允许进入关键部分。

 

下一篇将介绍Wait() 和 Pulse() 机制…