线程同步问题及其解决方案

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

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 ...
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485