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封裝一個線程安全的容器。

相關文章
相關標籤/搜索