随着对多线程学习的深刻,你可能以为须要了解一些有关线程共享资源的问题. .NET framework提供了不少的类和数据类型来控制对共享资源的访问。
考虑一种咱们常常遇到的状况:有一些全局变量和共享的类变量,咱们须要从不一样的线程来更新它们,能够经过使用System.Threading.Interlocked类完成这样的任务,它提供了原子的,非模块化的整数更新操做。
还有你可使用System.Threading.Monitor类锁定对象的方法的一段代码,使其暂时不能被别的线程访问。
System.Threading.WaitHandle类的实例能够用来封装等待对共享资源的独占访问权的操做系统特定的对象。尤为对于非受管代码的互操做问题。
System.Threading.Mutex用于对多个复杂的线程同步的问题,它也容许单线程的访问。
像ManualResetEvent和AutoResetEvent这样的同步事件类支持一个类通知其余事件的线程。
不讨论线程的同步问题,等于对多线程编程知之甚少,可是咱们要十分谨慎的使用多线程的同步。在使用线程同步时,咱们事先就要要可以正确的肯定是那个对象和方法有可能形成死锁(死锁就是全部的线程都中止了相应,都在等者对方释放资源)。还有赃数据的问题(指的是同一时间多个线程对数据做了操做而形成的不一致),这个不容易理解,这么说吧,有X和Y两个线程,线程X从文件读取数据而且写数据到数据结构,线程Y从这个数据结构读数据并将数据送到其余的计算机。假设在Y读数据的同时,X写入数据,那么显然Y读取的数据与实际存储的数据是不一致的。这种状况显然是咱们应该避免发生的。少许的线程将使得刚才的问题发生的概率要少的多,对共享资源的访问也更好的同步。
.NET Framework的CLR提供了三种方法来完成对共享资源 ,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。
(1) 代码域同步:使用Monitor类能够同步静态/实例化的方法的所有代码或者部分代码段。不支持静态域的同步。在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步,这在后面会讲到。
(2) 手工同步:使用不一样的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)建立本身的同步机制。这种同步方式要求你本身手动的为不一样的域和方法同步,这种同步方式也能够用于进程间的同步和对共享资源的等待而形成的死锁解除。
(3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象建立简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。全部在同一个上下文域的对象共享同一个锁。
Monitor Class
在给定的时间和指定的代码段只能被一个线程访问,Monitor 类很是适合于这种状况的线程同步。这个类中的方法都是静态的,因此不须要实例化这个类。下面一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。
Monitor.Enter 方法:在指定对象上获取排他锁。
Monitor.TryEnter 方法:试图获取指定对象的排他锁。
Monitor.Exit 方法:释放指定对象上的排他锁。
Monitor.Wait 方法:释放对象上的锁并阻塞当前线程,直到它从新获取该锁。
Monitor.Pulse 方法:通知等待队列中的线程锁定对象状态的更改。
Monitor.PulseAll 方法:通知全部的等待线程对象状态的更改。
经过对指定对象的加锁和解锁能够同步代码段的访问。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用来对指定对象的加锁和解锁。一旦获取(调用了Monitor.Enter)指定对象(代码段)的锁,其余的线程都不能获取该锁。举个例子来讲吧,线程X得到了一个对象锁,这个对象锁能够释放的(调用Monitor.Exit(object) or Monitor.Wait)。当这个对象锁被释放后,Monitor.Pulse方法和 Monitor.PulseAll方法通知就绪队列的下一个线程进行和其余全部就绪队列的线程将有机会获取排他锁。线程X释放了锁而线程Y得到了锁,同时调用Monitor.Wait的线程X进入等待队列。当从当前锁定对象的线程(线程Y)受到了Pulse或PulseAll,等待队列的线程就进入就绪队列。线程X从新获得对象锁时,Monitor.Wait才返回。若是拥有锁的线程(线程Y)不调用Pulse或PulseAll,方法可能被不肯定的锁定。Pulse, PulseAll and Wait必须是被同步的代码段鄂被调用。对每个同步的对象,你须要有当前拥有锁的线程的指针,就绪队列和等待队列(包含须要被通知锁定对象的状态变化的线程)的指针。
你也许会问,当两个线程同时调用Monitor.Enter会发生什么事情?不管这两个线程地调用Monitor.Enter是多么地接近,实际上确定有一个在前,一个在后,所以永远只会有一个得到对象锁。既然Monitor.Enter是原子操做,那么CPU是不可能偏好一个线程而不喜欢另一个线程的。为了获取更好的性能,你应该延迟后一个线程的获取锁调用和当即释放前一个线程的对象锁。对于private和internal的对象,加锁是可行的,可是对于external对象有可能致使死锁,由于不相关的代码可能由于不一样的目的而对同一个对象加锁。
若是你要对一段代码加锁,最好的是在try语句里面加入设置锁的语句,而将Monitor.Exit放在finally语句里面。对于整个代码段的加锁,你可使用MethodImplAttribute(在System.Runtime.CompilerServices命名空间)类在其构造器中设置同步值。这是一种能够替代的方法,当加锁的方法返回时,锁也就被释放了。若是须要要很快释放锁,你可使用Monitor类和C# lock的声明代替上述的方法。
让咱们来看一段使用Monitor类的代码:
public void some_method()
{
int a=100;
int b=0;
Monitor.Enter(this);
//say we do something here.
int c=a/b;
Monitor.Exit(this);
}
上面的代码运行会产生问题。当代码运行到int c=a/b; 的时候,会抛出一个异常,Monitor.Exit将不会返回。所以这段程序将挂起,其余的线程也将得不到锁。有两种方法能够解决上面的问题。第一个方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后必定会释放锁。第二种方法是:利用C#的lock()方法。调用这个方法和调用Monitoy.Enter的做用效果是同样的。可是这种方法一旦代码执行超出范围,释放锁将不会自动的发生。见下面的代码:
public void some_method()
{
int a=100;
int b=0;
lock(this);
//say we do something here.
int c=a/b;
}
C# lock申明提供了与Monitoy.Enter和Monitoy.Exit一样的功能,这种方法用在你的代码段不能被其余独立的线程中断的状况。
WaitHandle Class
WaitHandle类做为基类来使用的,它容许多个等待操做。这个类封装了win32的同步处理方法。WaitHandle对象通知其余的线程它须要对资源排他性的访问,其余的线程必须等待,直到WaitHandle再也不使用资源和等待句柄没有被使用。下面是从它继承来的几个类:
Mutex 类:同步基元也可用于进程间同步。
AutoResetEvent:通知一个或多个正在等待的线程已发生事件。没法继承此类。
ManualResetEvent:当通知一个或多个正在等待的线程事件已发生时出现。没法继承此类。
这些类定义了一些信号机制使得对资源排他性访问的占有和释放。他们有两种状态:signaled 和 nonsignaled。Signaled状态的等待句柄不属于任何线程,除非是nonsignaled状态。拥有等待句柄的线程再也不使用等待句柄时用set方法,其余的线程能够调用Reset方法来改变状态或者任意一个WaitHandle方法要求拥有等待句柄,这些方法见下面:
WaitAll:等待指定数组中的全部元素收到信号。
WaitAny:等待指定数组中的任一元素收到信号。
WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。
这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。
WaitHandle对象封装等待对共享资源的独占访问权的操做系统特定的对象不管是收管代码仍是非受管代码均可以使用。可是它没有Monitor使用轻便,Monitor是彻底的受管代码并且对操做系统资源的使用很是有效率。
Mutex Class
Mutex是另一种完成线程间和跨进程同步的方法,它同时也提供进程间的同步。它容许一个线程独占共享资源的同时阻止其余线程和进程的访问。Mutex的名字就很好的说明了它的全部者对资源的排他性的占有。一旦一个线程拥有了Mutex,想获得Mutex的其余线程都将挂起直到占有线程释放它。Mutex.ReleaseMutex方法用于释放Mutex,一个线程能够屡次调用wait方法来请求同一个Mutex,可是在释放Mutex的时候必须调用一样次数的Mutex.ReleaseMutex。若是没有线程占有Mutex,那么Mutex的状态就变为signaled,不然为nosignaled。一旦Mutex的状态变为signaled,等待队列的下一个线程将会获得Mutex。Mutex类对应与win32的CreateMutex,建立Mutex对象的方法很是简单,经常使用的有下面几种方法:
一个线程能够经过调用WaitHandle.WaitOne 或 WaitHandle.WaitAny 或 WaitHandle.WaitAll获得Mutex的拥有权。若是Mutex不属于任何线程,上述调用将使得线程拥有Mutex,并且WaitOne会当即返回。可是若是有其余的线程拥有Mutex,WaitOne将陷入无限期的等待直到获取Mutex。你能够在WaitOne方法中指定参数即等待的时间而避免无限期的等待Mutex。调用Close做用于Mutex将释放拥有。一旦Mutex被建立,你能够经过GetHandle方法得到Mutex的句柄而给WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。
下面是一个示例:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
FirstMutex.WaitOne();
//some kind of processing can be done here.
Int x=a/b;
FirstMutex.Close();
}
在上面的例子中,线程建立了Mutex,可是开始并无申明拥有它,经过调用WaitOne方法拥有Mutex。
Synchronization Events
同步时间是一些等待句柄用来通知其余的线程发生了什么事情和资源是可用的。他们有两个状态:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是这种同步事件。
AutoResetEvent Class
这个类能够通知一个或多个线程发生事件。当一个等待线程获得释放时,它将状态转换为signaled。用set方法使它的实例状态变为signaled。可是一旦等待的线程被通知时间变为signaled,它的转台将自动的变为nonsignaled。若是没有线程侦听事件,转台将保持为signaled。此类不能被继承。
ManualResetEvent Class
这个类也用来通知一个或多个线程事件发生了。它的状态能够手动的被设置和重置。手动重置时间将保持signaled状态直到ManualResetEvent.Reset设置其状态为nonsignaled,或保持状态为nonsignaled直到ManualResetEvent.Set设置其状态为signaled。这个类不能被继承。
Interlocked Class
它提供了在线程之间共享的变量访问的同步,它的操做时原子操做,且被线程共享.你能够经过Interlocked.Increment 或 Interlocked.Decrement来增长或减小共享变量.它的有点在因而原子操做,也就是说这些方法能够代一个整型的参数增量而且返回新的值,全部的操做就是一步.你也可使用它来指定变量的值或者检查两个变量是否相等,若是相等,将用指定的值代替其中一个变量的值.