.NET多线程编程(3)――线程同步

    随着对多线程学习的深入 , 你可能觉得需要了解一些有关线程共享资源的问题 . .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来增加或减少共享变量.它的有点在于是原子操作,也就是说这些方法可以代一个整型的参数增量并且返回新的值,所有的操作就是一步.你也可以使用它来指定变量的值或者检查两个变量是否相等,如果相等,将用指定的值代替其中一个变量的值.

你可能感兴趣的:(多线程,.net,线程同步,职场,休闲)