C#线程同步(1)- 临界区&Lock

文章原始出处 http://xxinside.blogbus.com/logs/46441956.htmlhtml

预备知识:线程的相关概念和知识,有多线程编码的初步经验。web

  一个机会,索性把线程同步的问题在C#里面的东西都粗略看了下。数据库

  第一印象,C#关于线程同步的东西好多,保持了C#一向的大杂烩和四不象风格(Java/Delphi)。临界区跟Java差很少只不过关键字用lock替代了synchronized,而后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,另外又搞出来几个Event……让人甚是不明了。无论那么多,一个一个来吧。编程

临界区(Critical Section)安全

  是一段在同一时候只被一个线程进入/执行的代码。为啥要有这个东西?多线程

  1. 是由于这段代码访问了“临界资源”,而这种资源只能同时被互斥地访问。举个例子来讲,你的银行帐户就是一个互斥资源,一个银行系统里面改变余额(存取)的操做代码就必须用在临界区内。若是你的帐户余额是$100,000(若是是真的,那么你就不用再往下看了,仍是睡觉去吧),假设有两我的同时给你汇款$50,000。有两个线程分别执行这两笔汇款业务,线程A在获取了你的帐户余额后,在它把新余额($150000)储存回数据库之前,操做系统把这个线程暂停转而把CPU的时间片分给另外一个线程(是的,这太巧了);那么线程B此时取出的帐户余额仍然是$10000,随后线程B幸运的获得的CPU时间把$50000存入你的帐户,那么余额变成$150000。而此后某个时候,线程A再次得以执行,它也把“新”余额$150000更新到系统……因而你的$50000就这么凭空消失了。(此段省去常见到一个示例图,请自行想象)
  2. 是由于OS的多任务调度,其实在缘由一里面已经提到。若是OS不支持多任务调度,那么线程A/线程B执行更新余额的操做老是一个接一个进行,那么彻底不会有上面的问题了。在多线程的世界里,你必须随时作好你的代码执行过程随时失去控制的准备;你须要好好考虑当代码从新执行的时候,是否能够继续正确的执行。一句话,你的程序段在多线程的世界里,你所写的方法并非“原子性”的操做。

Lock关键字并发

  C#提供lock关键字实现临界区,MSDN里给出的用法:less

Object thisLock = new Object();dom

lock (thisLock)ide

{   

// Critical code section

}

  lock实现临界区是经过“对象锁”的方式,注意是“对象”,因此你只能锁定一个引用类型而不能锁定一个值类型。第一个执行该代码的线程,成功获取对这个对象的锁定,进而进入临界区执行代码。而其它线程在进入临界区前也会请求该锁,若是此时第一个线程没有退出临界区,对该对象的锁定并无解除,那么当前线程会被阻塞,等待对象被释放。

  既然如此,在使用lock时,要注意不一样线程是否使用同一个“锁”做为lock的对象。如今回头来看MSDN的这段代码彷佛很容易让人误解,容易让人联想到这段代码是在某个方法中存在,觉得thisLock是一个局部变量,而局部变量的生命周期是在这个方法内部,因此当不一样线程调用这个方法的时候,他们分别请求了不一样的局部变量做为锁,那么他们均可以分别进入临界区执行代码。所以在MSDN随后真正的示例中,thisLock其实是一个private的类成员变量:

using System; using System.Threading;

class Account {    

private Object thisLock = new Object();

    int balance;

    Random r = new Random();

    public Account(int initial)    

{        

balance = initial;  

}

  int Withdraw(int amount)

 {

        // This condition will never be true unless the lock statement  

       // is commented out:        

if (balance < 0)       

 {            

   throw new Exception("Negative Balance");        

}

        // Comment out the next line to see the effect of leaving out 

        // the lock keyword:   

      lock(thisLock)       

  {            

if (balance >= amount)      

       {                

Console.WriteLine("Balance before Withdrawal :  " + balance);        

Console.WriteLine("Amount to Withdraw        : -" + amount);  

 balance = balance - amount;

 Console.WriteLine("Balance after Withdrawal  :  " + balance);  

    return amount;  

       }            

else            

{                

return 0; // transaction rejected  

 }

 }

 }

    public void DoTransactions()    

{      

   for (int i = 0; i < 100; i++)

        {

            Withdraw(r.Next(1, 100));

        }

    }

}

class Test

{   

  static void Main()

    {        

      Thread[] threads = new Thread[10];  

       Account acc = new Account(1000);

        for (int i = 0; i < 10; i++)

        {         

      Thread t = new Thread(new ThreadStart(acc.DoTransactions));       

           threads[i] = t;

        }       

      for (int i = 0; i < 10; i++)

        {            

            threads[i].Start();

        }   

  }

}

  这个例子中,Account对象只有一个,因此临界区所请求的“锁”是惟一的,所以用类的成员变量是能够实现互斥意图的,其实用你们一般喜欢的lock(this)也何尝不可,也即请求这个Account实例自己做为锁。可是若是在某种状况你的类实例并不惟一或者一个类的几个方法之间都必需要互斥,那么就要当心了。必须牢记一点,全部由于同一互斥资源而须要互斥的操做,必须请求“同一把锁”才有效。

  假设这个Account类并不仅有一个Withdraw方法修改balance,而是用Withdraw()来特定执行取款操做,另有一个Deposit()方法专门执行存款操做。很显然这两个方法必须是互斥执行的,因此这两个方法中所用到的锁也必须一致;不能一个用thisLock,另外一个从新用一个private Object thisLock1 = new Object()。再进一步,其实这个操做场景下各个互斥区存在的目的是由于有“Balance”这个互斥资源,全部有关Balance的地方应该都是互斥的(若是你不介意读取操做读到的是脏数据的话,固然也能够不用)。

题外话:   这么看来其实用Balance自己做为锁也许更为符合“逻辑”,lock住须要互斥的资源自己不是更好理解么?不过这里Balance是一个值类型,你并不能直接对它lock(你可能须要用到volatile关键字,它能在单CPU的状况下确保只有一个线程修改一个变量)。

Lock使用的建议

  关于使用Lock微软给出的一些建议。你可以在MSDN上找到这么一段话:

  一般,应避免锁定 public 类型,不然实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:   1.若是实例能够被公共访问,将出现 lock (this) 问题。   2.若是 MyType 能够被公共访问,将出现 lock (typeof (MyType)) 问题。   3.因为进程中使用同一字符串的任何其余代码将共享同一个锁,因此出现 lock("myLock") 问题。    4.最佳作法是定义 private 对象来锁定, 或 private static 对象变量来保护全部实例所共有的数据。

  lock(this)的问题我是这么理解:

  1. 处于某种缘由Account在整个程序空间内不是惟一,那么不一样Account实例的相应方法就不可能互斥,由于他们请求的是不一样Accout实例内部的不一样的锁。这时候微软示例中的private Object thisLock仍然也避免不了这个问题,而须要使用private static Object thisLock来解决问题,由于static变量是全部类实例共享的。
  2. 猜测就算Account只有一个实例,可是若是在程序内部被多个处理不一样任务的线程访问,那么Account实例可能会被某段代码直接做为锁锁定;这至关于你本身锁定了本身,而别人在不告诉你的状况下也能够能锁定你。这些状况都是你在写Account这个类的时候并无办法做出预测的,因此你的Withdraw代码可能被挂起,在多线程的复杂状况下也容易形成死锁。无论怎样,你写这段代码的时候确定不会期待外部的代码跟你使用了同一把锁吧?这样很危险。另外,从面向对象来讲,这等于把方法内部的东西隐式的暴露出去。为了实现互斥,专门创建不依赖系this的代码机制老是好的;thisLock,专事专用,是个好习惯。

  MyType的问题跟lock(this)差很少理解,不过比lock(this)更严重。由于Lock(typeof(MyType))锁定住的对象范围更为普遍,因为一个类的全部实例都只有一个类对象(就是拥有Static成员的那个对象实例),锁定它就锁定了该对象的全部实例。同时lock(typeof(MyType))是个很缓慢的过程,而且类中的其余线程、甚至在同一个应用程序域中运行的其余程序均可以访问该类型对象,所以,它们都有可能锁定类对象,彻底阻止你代码的执行,致使你本身代码的挂起或者死锁。

  至于lock("myLock"),是由于在.NET中字符串会被暂时存放。若是两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。因此若是有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。

.NET集合类对lock的支持

  在多线程环境中,常会碰到的互斥资源应该就是一些容器/集合。所以.NET在一些集合类中(好比ArrayList,HashTable,Queue,Stack,包括新增的支持泛型的List)已经提供了一个供lock使用的对象SyncRoot。

  在.Net1.1中大多数集合类的SyncRoot属性只有一行代码:return this,这样和lock(集合的当前实例)是同样的。不过ArrayList中的SyncRoot有所不一样(这个并非我反编译的,我并无验证这个说法):

get

   

  if(this._syncRoot==null)

    {    

      Interlocked.CompareExchange(ref this._syncRoot,newobject(),null);

   }

     returnthis._syncRoot; 

}

题外话:   

上面反编译的ArrayList的代码,引出了个Interlocked类,即互锁操做,用以对某个内存位置执行的简单原子操做。举例来讲在大多数计算机上,增长变量操做不是一个原子操做,须要执行下列步骤:

  1. 将实例变量中的值加载到寄存器中。
  2. 增长或减小该值。
  3. 在实例变量中存储该值。

  线程可能会在执行完前两个步骤后被夺走CPU时间,而后由另外一个线程执行全部三个步骤。当第一个线程从新再开始执行时,它改写实例变量中的值,形成第二个线程执行增减操做的结果丢失。这根咱们上面提到的银行帐户余额的例子是一个道理,不过是更微观上的体现。咱们使用该类提供了的Increment和Decrement方法就能够避免这个问题。
  另外,Interlocked类上提供了其它一些能保证对相关变量的操做是原子性的方法。如Exchange()能够保证指定变量的值交换操做的原子性,Read()保证在32位操做系统中对64位变量的原子读取。而这里使用的CompareExchange方法组合了两个操做:保证了比较和交换操做按原子操做执行。此例中CompareExchange方法将当前syncRoot和null作比较,若是相等,就用new object()替换SyncRoot。
  在现代处理器中,Interlocked 类的方法常常能够由单个指令来实现,所以它们的执行性能很是高。虽然Interlocked没有直接提供锁定或者发送信号的能力,可是你能够用它编写锁和信号,从而编写出高效的非阻止并发的应用程序。可是这须要复杂的低级别编程能力,所以大多数状况下使用lock或其它简单锁是更好的选择。

 

 

 

  看到这里是否是已经想给微软一耳光了?一边教导你们不要用lock(this),一边居然在基础类库中大量使用……呵呵,我只能说据传从.Net2.0开始SyncRoot已是会返回一个单独的类了,想来大约应该跟ArrayList那种实现差很少,有兴趣的能够反编译验证下。

  这里想说,代码是本身的写的,最好减小本身代码对外部环境的依赖,事实证实即使是.Net基础库也不是那么可靠。本身能想到的问题,最好本身写代码去处理,须要锁就本身声明一个锁;再也不须要一个资源那么本身代码去Dispose掉(若是是实现IDisposable接口的)……不要想着什么东西系统已经帮你作了。你永远没法保证你的类将会在什么环境下被使用,你也没法预见到下一版的Framework是否偷偷改变了实现。当你代码莫名其妙不Work的时候,你是很难找出由这些问题引起的麻烦。只有你代码足够的独立(这里没有探讨代码耦合度的问题),才能保证它足够的健壮;别人代码的修改(哪怕是你看来“不当”的修改),形成你的Code没法工做不是总有些好笑么(我还想说“苍蝇不叮无缝的蛋”“不要由于别人的错误连累本身”)?

  一些集合类中还有一个方法是和同步相关的:Synchronized,该方法返回一个集合的内部类,该类是线程安全的,由于他的大部分方法都用lock来进行了同步处理(你会不会想那么SyncRoot显得多余?别急。)。好比,Add方法会相似于:

public override void Add(objectkey,objectvalue)      lock(this._table.SyncRoot)   {     this._table.Add(key,value);   }   }

  不过即使是这个Synchronized集合,在对它进行遍历时,仍然不是一个线程安全的过程。当你遍历它时,其余线程仍能够修改该它(Add、Remove),可能会致使诸以下标越界之类的异常;就算不出错,你也可能读到脏数据。若要在遍历过程当中保证线程安全,还必须在整个遍历过程当中锁定集合,我想这才是SynRoot存在的目的吧:

Queue myCollection = newQueue(); lock(myCollection.SyncRoot) {   foreach(ObjectiteminmyCollection)   {       //Insert your code here.   }   }

  提供SynRoot是为了把这个已经“线程安全”的集合内部所使用的“锁”暴露给你,让你和它内部的操做使用同一把锁,这样才能保证在遍历过程互斥掉其它操做,保证你在遍历的同时没有能够修改。另外一个能够替代的方法,是使用集合上提供的静态ReadOnly()方法,来返回一个只读的集合,并对它进行遍历,这个返回的只读集合是线程安全的。

  到这里彷佛关于集合同步的方法彷佛已经比较清楚了,不过若是你是一个很迷信MS基础类库的人,那么此次恐怕又会失望了。微软决定全部从那些自Framwork 3.0以来加入的支持泛型的集合中,如List,取消掉建立同步包装器的能力,也就是它们再也不有Synchronized,IsSynchronized也总会返回false;而ReadOnly这个静态方法也变为名为AsReadOnly的实例方法。做为替代,MS建议你仍然使用lock关键字来锁定整个集合。

  至于List之类的泛型集合SyncRoot是怎样实现的,MSDN是这样描述的“在 List<(Of <(T>)>) 的默认实现中,此属性始终返回当前实例。”,赶忙去吐血吧!

本身的SyncRoot

仍是上面提过的老话,靠本身,以不变应万变:

public class MySynchronizedList

{  

  private readonly object syncRoot = new object();

  private readonly List<intlist = new List<int>();

  public object SyncRoot  

  {    

       get

       {

           return this.syncRoot;

       }

  }

  public void Add(int i)

  {    

      lock(syncRoot)

     {      

        list.Add(i);

    }

  }

  //...

}

自已写一个类,用本身的syncRoot封装一个线程安全的容器。

相关文章
相关标签/搜索