在多线程编程中,一个常见的问题是多个线程同时访问同一资源。这种情况可能会导致程序崩溃或者数据损坏。例如,两个线程尝试打开同一个文件,或者一个线程在写入文件时另一个线程尝试截断它。为了避免这些问题,.NET框架提供了多种同步机制,Monitor类是其中之一。本文将介绍如何使用Monitor类来实现线程同步。
首先,来看一个简单的示例程序,它模拟了线程同步问题。在这个程序中,有两个线程,它们都试图增加同一个整型资源x。通过两个for循环和一个Sleep函数来模拟实际的计算过程。理论上,期望在屏幕上看到从0到19的20个数字按顺序显示。
    #using <mscorlib.dll>
    using namespace System;
    using namespace System::Threading;
    __gc class AddThread {
    public:
        int x;
        void Add1();
        AddThread();
        Thread *t1, *t2;   
    };
    int wmain(void) {
        AddThread *a = new AddThread();
        return 0;
    }
    AddThread::AddThread() {
        x = 0;
        t1 = new Thread(new ThreadStart(this, &AddThread::Add1));
        t2 = new Thread(new ThreadStart(this, &AddThread::Add1));
        t1->Name = "Thread 1";
        t2->Name = "Thread 2";
        t1->Start();
        t2->Start();
    }
    void AddThread::Add1() {
        for (int j = 0; j < 10; j++) {
            for (int t = 0; t < 5000; t++)
                x++;
            Thread::Sleep(5);
            for (int t = 0; t < 5000; t++)
                x--;
            Console::WriteLine("\n{0}.....{1} has incremented x", __box(x++), Thread::CurrentThread->Name);			
        }
    }
    
编译并运行这个程序,可能会看到类似于以下的输出,这取决于处理器速度和当前负载。
    5000.....Thread 1 has incremented x
    5001.....Thread 2 has incremented x
    5002.....Thread 1 has incremented x
    ...
    
显然,这不是想要的结果。问题在于一个线程正在处理x,而另一个线程也在访问它。那么,如何防止这种情况发生呢?
Monitor类可以帮助锁定资源,以防止其他线程访问它,然后在使用完毕后释放它。以下是如何修改Add1成员函数的示例:
    void AddThread::Add1() {
        for (int j = 0; j < 10; j++) {
            Monitor::Enter(this);
            for (int t = 0; t < 5000; t++)
                x++;
            Thread::Sleep(5);
            for (int t = 0; t < 5000; t++)
                x--;
            Console::WriteLine("\n{0}.....{1} has incremented x", __box(x++), Thread::CurrentThread->Name);				
            Monitor::Exit(this);
        }
    }
    
现在再次编译并运行程序,将得到期望的输出。
    0.....Thread 1 has incremented x
    1.....Thread 2 has incremented x
    ...
    
Monitor::Enter(this)会阻止(即等待)如果其他线程已经锁定了资源(在例子中,传递了this指针)。如果对象是空闲的,那么Enter将获得该对象的监视器锁。Monitor::Exit(this)将释放该资源的监视器锁。它们都是Monitor类的静态成员函数。
在同一个线程中,可以多次调用Enter,它不会阻止;但必须确保Exit调用的次数与Enter调用的次数相同。在某些情况下,阻止可能是不可取的。在这种情况下,可以使用一个叫做TryEnter的替代调用。它有三个重载。感兴趣的是:
    public:
    static bool TryEnter(Object * obj);
    
它不会阻止,但如果锁定了,会返回true;如果资源已经被锁定,会返回false。TryEnter还有其他重载,允许阻塞指定的时间间隔。可以在.NET Framework SDK文档中查找Monitor::TryEnter。
假设希望线程2在线程1开始工作之前完成前六个数字。在这里,可以利用Wait和Pulse成员函数。以下是如何再次修改Add1函数的示例:
    void AddThread::Add1() {
        for (int j = 0; j < 10; j++) {
            Monitor::Enter(this);
            if (Thread::CurrentThread->Name->Equals("Thread 1")) {
                if (x < 5)
                    Monitor::Wait(this);
            } else {
                if (x > 4)
                    Monitor::Pulse(this);
            }
            for (int t = 0; t < 5000; t++)
                x++;
            Thread::Sleep(5);
            for (int t = 0; t < 5000; t++)
                x--;
            Console::WriteLine("\n{0}.....{1} has incremented x", __box(x++), Thread::CurrentThread->Name);				
            Monitor::Exit(this);
        }
    }
    
Wait的作用是释放监视器,但指示CLR(公共语言运行库)在监视器再次空闲时期望重新获得监视器。Pulse指示CLR发生了状态变化,这可能会释放一个等待的线程。CLR会跟踪所有等待的线程,并按照它们调用Wait的顺序释放它们。
现在再次编译并运行程序,将得到以下输出:
    0.....Thread 2 has incremented x
    1.....Thread 2 has incremented x
    ...