C# 多线程之线程同步
多线程间应尽量避免同步问题,最好不要线程间共享数据。如果必须要共享数据,就需要使用同步技术,确保一次只有一个线程访问和改变共享状态。
一::lock语句
lock语句事设置锁定和接触锁定的一种简单方法。其语法非常简单:
lock (obj) { // 需要发生同步的代码区 }
将共享数据的操作代码,放在上述的“{...}”区域内。锁定的对象(obj)必须是引用类型,如果锁定一个值类型,实际是锁定了它的一个副本,并没有实现锁定功能。
一般地,被锁定对象需要被创建为 私有 只读 引用类型:
private readonly object obj = new object();
二::Interlocked类
Interlocked类用于使变量的简单语句原子化。它提供了以线程安全的方式递增、递减、交换和读取值的方法。
private int stateFlag = 0; public int IncrementState { //get //{ // lock (this) // { // stateFlag++; // return stateFlag; // } //} get { return Interlocked.Increment(ref stateFlag); // using System.Threading; //Interlocked.Decrement(ref V0); //Interlocked.Exchange(ref V1, ref V2); //Interlocked.Read(ref V0); } }
三::Monitor类
与lock相似,C#的lock语句被编译器解析为使用Monitor类。锁定开始相当于 Monitor.Enter(obj) 方法,该方法会一直等待,直到线程被对象锁定。解除锁定后线程进入同步阶段,使用 Monitor.Exit(obj)方法解除锁定,编译器将它与try块的finally结合。方法一中的代码,相当于:
Monitor.Enter(obj); try { // 需要发生同步的代码区 } finally { Monitor.Exit(obj); }
与lock语句相比,Monitor类的优点在于:可以添加一个等待北锁定的超时值。这样就不会无限期等待被锁定,而可以使用 TryEnter() 方法,给一个超时参数。
bool lockTaken = false; Monitor.TryEnter(obj, 500, ref lockTaken); if (lockTaken) { try { // acquired the lock // synchronized region for obj } finally { Monitor.Exit(obj); } } else { // didn't get the lock,do something else }
如果obj被锁定,TryEnter() 方法就会把 bool 型引用参数 lockTaken 设置为 true,并同步地访问由 obj 锁定的状态。如果另一线程 锁定 obj 的时间超过 500 毫秒,Try Enter() 方法就把变量 lockTaken 设为 false ,线程不再等待,而是用于执行其它操作。也许在之后,该线程会尝试再次被锁定。
四::SpinLock结构
它是一个结构体(struct),用法极类似于Monitor类。获得锁用 Enter()或TryEnter() 方法,释放锁用 Exit() 方法。它还提供了属性 IsHeld 和 IsHeldByCurrentThred ,指定当前是否被锁定。
SpinLock mSpinLock = new SpinLock(); // 最好只是用一个 SpinLock public void fun1() { // ..... bool lockTaken = false; mSpinLock.Enter(ref lockTaken); try { // synchronized region } finally { mSpinLock.Exit(); } // ... } public void fun2() { // ..... bool lockTaken = false; mSpinLock.TryEnter(500, ref lockTaken); if (lockTaken) { try { // synchronized region } finally { mSpinLock.Exit(); } } else { // didn't get the lock,do something else } // ... }
SpinLock结构体是 .Net 4 新增。它适用于:有大量的锁,且锁定时间都非常短。程序需要避免使用多个 SpinLock 结构,也不要调用任何可能阻塞的内容。
五::WaitHandle 基类
WaitHandle是一个抽象基类,用于等待一个信号的设置。可以等待不同的信号,因为WaitHandle是一个基类,可以从中派生一些类。
public delegate int TakesAWhileDelegate(int data, int ms); // 声明委托 public void Main() { TakesAWhileDelegate vTAwdl = TakesAWhile; IAsyncResult vAr = vTAwdl.BeginInvoke(1, 3000, null, null); while(true) { Console.Write("."); if (vAr.AsyncWaitHandle.WaitOne(300, false)) // 等待 vAr.AsyncWaitHandle 收到信号(超时300毫秒) { Console.WriteLine("Can get the result now."); break; } } int result = vTAwdl.EndInvoke(vAr); Console.WriteLine("Result:{0}", result);
Console.Read(); } int TakesAWhile(int data, int ms) { Console.WriteLine("TakesAWhile started"); Thread.Sleep(ms); Console.WriteLine("TakesAWhile completed"); return ++data; }
以上实例代码,使用”异步委托", BeginInvoke() 方法返回一个实现了 IAsycResult接口的对象。使用 IAsycResult 接口,可以用AsycResult属性访问 WaitHandle 基类。在调用WaitOne()方法时,线程等待一个与等待句柄相关的信号。
使用 WaitHandle 类可以等待一个信号出现(WaitOne()方法)、等待必须发出信号的多个对象(WaitAll()方法)、或者等待多个对象中的一个(WaitAny()方法)。后两者事WaitHandle类的静态方法,接收一个WaitHandle参数数组。
六::Mutex类
Mutex(mutual exclusion,互斥)是 .NET Framework中提供跨多个进程同步访问的一个类。所以,它常被用于“程序单一启动控制”。
/// <summary> /// 单一进程 检查,如果已经运行一个进程,返回false,表示检查不通过。否则返回true。 /// </summary> /// <returns></returns> private bool RunOnceCheck() { bool vExist; Mutex nMutex = new Mutex(false, "SingletonWinAppMutex", out vExist); if (!vExist) { // 表示已经启动一个了,应退出当前启动 return false; } return true; }
它非常类似于Monitor类,因为他们都只有一个线程能拥有锁定。只有一个线程能获得互斥锁定,访问受互斥保护的同步代码区域。Mutex派生自基类WaitHandle,因此可以利用WaitOne()方法获得互斥锁定,在该过程中成为该互斥的拥有者。调用 ReleaseMutex()方法,释放互斥。
bool createdNew; Mutex mutex = new Mutex(false, "ProCSharpMutex", out createdNew); if (mutex.WaitOne()) { try { // synchronized region } finally { mutex.ReleaseMutex(); } } else { // some problem happened while waiting }
七::Semaphore类
Semaphore非常类似于互斥,其区别在于Semaphore可以同时由多个线程使用。它是一种计数互斥锁定,可以定义允许同时访问受其锁定保护的资源的线程个数。它适用于:有许多可用资源,且只允许一定数量的线程访问该资源。
八::Events类
它是一种可以在系统范围内同步资源的方法。
九::Barrier类
它非常适用于其中工作有很多个任务分支且以后又需要合并工作的情况。
十::ReaderWriterLockSlim类
为了使锁定机制允许锁定多个读取器(而不是一个写入器)访问某个资源,可以使用此类。它提供了一个锁定功能,如果没有写入器锁定资源,就允许多个读取器访问资源,但只能有一个写入器锁定该资源。