.Net学习难点讨论系列5 – 线程同步问题之二
接上篇文章,来说一下托管代码包装Windows内核对象完成线程同步的方法
Windows提供的用于同步的内核对象包括:互斥体、信号量和事件。CLR中System.Threading命名空间下的WaitHandle类(抽象基类)完成了对这些内核对象的包装,包装内核对象在托管方式下实现同步的类也自然都继承自WaitHandle类,包括Mutex,Semaphore与EventWaitHandle(基类)。以上提到的这四个类都覆写了WaitOne()方法并实现了WaitAll()、WaitAny()与SignalAndWait()静态方法,这四个方法分别允许等待对象收到信号或者数组中的所有对象收到信号,等待数组中至少一个对象收到信号以及向一个对象发出信号并等待另一个对象。以上这些类必须实例化后才可以使用,因此这里所要考虑的对象不是被同步的对象而是用于同步的对象,也就意味着作为参数传递给WaitAll()、WaitAny()以及SignalAndWait()静态方法的对象本身全部是互斥体、事件或者信号量。
首先看一下互斥体的使用
方法5:使用Mutex类
使用互斥体可以在同一台机器甚至是多台机器上(需要.Net Remoting配合)的多个进程中使用同一个互斥体。跨多进程同步是Mutex相对与Monitor类的最大特点,所以,如果需要同步的访问处于一个进程之中,应该优先使用Monitor代替互斥体,前者的效率要高很多。
下面示例构建了一个有名互斥体来保护对资源的访问,该互斥体被同一台机器上的多个进程共享。
// 创建名为‘MutexTest’的互斥体
Mutex mutexFile = new Mutex(false, "MutexTest");
for (int i = 0; i < 10; i++){
mutexFile.WaitOne();
// 打开文件,写入‘Hello i’并关闭文件。
FileInfo fi = new FileInfo("tmp.txt");
StreamWriter sw = fi.AppendText();
sw.WriteLine("Hello {0}", i);
sw.Flush();
sw.Close();
System.Console.WriteLine("Hello {0}", i);
// 等待1秒以展示互斥体的作用
Thread.Sleep(1000);
mutexFile.ReleaseMutex();
}
mutexFile.Close();
}
WaitOne()方法会阻塞当前线程,知道获得互斥体,使用ReleaseMutex()方法可以释放互斥体。
注意,程序中new Mutex()不一定是创建了一个互斥体,可能是创建一个对互斥体"MutexTest"的引用。只有当该互斥体不存在时,系统才创建它。(只有没有其他进程引用时,Close()才真正销毁它)。
方法6:事件
事件被用于在线程之间传递通知,该通知表示某个事件发生了。被关注的事件与AutoResetEvent和ManualResetEvent这两个事件类的一个实例关联。(使用EventResetMode中AutoReset或ManualReset两个枚举类型之一可以避免创建一个新实例)
具体来说一个线程通过调用代表事件的对象的WaitOne()这个阻塞方法来等待一个事件被触发。另一个线程调用事件对象的Set()方法以触发事件从而使一个线程继续执行。
static void Main() {
events = new EventWaitHandle[2];
// 初始化事件状态:false
events[0] = new EventWaitHandle( false ,EventResetMode.AutoReset );
events[1] = new EventWaitHandle( false ,EventResetMode.AutoReset );
Thread t0 = new Thread( ThreadProc0 );
Thread t1 = new Thread( ThreadProc1 );
t0.Start(); t1.Start();
AutoResetEvent.WaitAll( events );
Console.WriteLine( "MainThread: Thread0 reached 2" +
" and Thread1 reached 3." );
t0.Join();t1.Join();
}
static void ThreadProc0() {
for ( int i = 0; i < 5; i++ ) {
Console.WriteLine( "Thread0: {0}", i );
if ( i == 2 ) events[0].Set();
Thread.Sleep( 100 );
}
}
static void ThreadProc1() {
for ( int i = 0; i < 5; i++ ) {
Console.WriteLine( "Thread1: {0}", i );
if ( i == 3 ) events[1].Set();
Thread.Sleep( 60 );
}
}
手动重置与自动重置的区别:如果几个线程在等待同一个自动重置的事件,那么该事件需要为每个线程都触发一次,而在手动重置的情况下只需要简单的触发一次事件就可以让所有的阻塞线程继续执行。
方法7:信号量
信号量 – Semaphore类的实例用来限制对一个资源的并发访问数。当调用WaitOne()方法试图进入一个信号量时,如果当时已经进入的线程数量达到了某个最大值时,该线程就被阻塞。这个最大的入口的数量由Semaphore类的构造函数的第二个参数设定,而第一个参数则定义了初始时的入口数量。如果第一个参数的值小于第二个,线程在调用构造函数时会自动地占据二者插值数量的入口。最后这点也说明了同一个线程可以占据同一个信号量的多个入口。
示例:
static void Main() {
// 初始化空的槽数 : 2.
// 可同时使用的槽的最大数量 : 5.
// 主线程拥有的槽数 : 3 (5-2).
semaphore = new Semaphore( 2, 5 );
for ( int i = 0; i < 3; ++i ) {
Thread t = new Thread( WorkerProc );
t.Name = "Thread" + i;
t.Start();
Thread.Sleep( 30 );
}
}
static void WorkerProc() {
for ( int j = 0; j < 3; j++ ) {
semaphore.WaitOne();
Console.WriteLine( Thread.CurrentThread.Name + ": Begin" );
// 模拟一个.2s的任务
Thread.Sleep( 200 );
Console.WriteLine( Thread.CurrentThread.Name + ": End" );
semaphore.Release();
}
}
以上程序中,主线程占据了该信号量的3个入口,迫使另外3个线程去共享剩下的2个入口。从运行结果你可以看出并发工作的子线程数从未超过2个。
目前我了解到的线程同步方法就这两篇中介绍的7种,欢迎补充!