在多线程编程中,一个常见的问题是多个线程同时访问同一资源。这种情况可能会导致程序崩溃或者数据损坏。例如,两个线程尝试打开同一个文件,或者一个线程在写入文件时另一个线程尝试截断它。为了避免这些问题,.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
...