.Net執行緒同步技術解讀

  • 2019 年 12 月 5 日
  • 筆記

C#開發者(面試者)都會遇到lock(Monitor),Mutex,Semaphore,SemaphoreSlim這四個與鎖相關的C#類型,本文期望以最簡潔明了的方式闡述四種對象的區別。

什麼是執行緒安全

教條式理解

如果程式碼在多執行緒環境中運行的結果與單執行緒運行結果一樣,其他變數值也和預期是一樣的,那麼執行緒就是安全的;

結合場景理解

兩個執行緒都為集合增加元素,我們錯誤的理解即使是多執行緒也總有先後順序吧,集合的兩個位置先後塞進去就完了;實際上集合增加元素這個行為看起來簡單,實際並不一定是原子操作。

在添加一個元素的時候,它可能會有兩步來完成:

  1. 在 Items[Size] 的位置存放此元素;
  2. 增大 Size 的值。
  • 在單執行緒運行的情況下,如果 Size = 0,添加一個元素後,此元素在位置0,之後設置Size=1;
  • 如果是在多執行緒場景下,有兩個執行緒,執行緒A先將元素存放在位置0,但是此時CPU調度執行緒A暫停,執行緒B得到運行機會;執行緒B也向此ArrayList添加元素,因為此時Size仍然等於0 (注意哦,我們假設添加元素是經過兩個步驟,而執行緒A僅僅完成了步驟1),所以執行緒B也將元素存放在位置0。然後執行緒A和執行緒B都繼續運行,都增加 Size 的值。那好,我們來看看ArrayList的情況,元素實際上只有一個,存放在位置 0,而Size卻等於2,形成了臟數據,這種就定義為對ArrayList的新增元素操作是執行緒不安全的。

執行緒安全這個問題不單單存在於集合類,我們始終要記得: Never ever modify a shared resource by multipie threads unless resource is thread-safe.

我們對SqlServer,Mongodb,對HttpContext的訪問都會涉及thread-safe。

– 利用C# mongodb driver操作Mongo打包時常用操作是執行緒安全的,Only a few of the C# Driver classes are thread safe. Among them: MongoServer, MongoDatabase, MongoCollection and MongoGridFS.

– 對於HttpContext 靜態屬性的操作是執行緒安全的:Any public static members of this type (HttpContext) are thread safe, any instance members are not guaranteed to be thread safe.

各語言推出了適用於不同範圍的執行緒同步技術來預防以上臟數據(實現執行緒安全)

執行緒同步技術

話不多說,給出大圖:

四象限對象的區別:

  • 支援執行緒進入的個數
  • 是否跨進程支援

上半區 lock(Monitor), Mutex(中文稱為互斥鎖)都只支援單執行緒進入被保護程式碼,其他執行緒則必須等待進入的執行緒完成 {Critical Section}

下半區SemaphoreSlim、Semaphore(中文稱為訊號量)支援並發多執行緒進入被保護程式碼,對象在初始化時會指定 最大訊號燈數量,當執行緒請求訪問資源,訊號量遞減,而當他們釋放時,訊號量計數又遞增。

左半區lock (Monitor)、SemaphoreSlim 是CRL對象, 進程內執行緒同步; 右半區Mutex,Semaphore都繼承自WaitHandle對象,支援命名,是內核對象,在系統級別能支援進程間執行緒同步。

進程間執行緒同步不多見(分散式鎖的場景越來越多,這裡按下不表),啰嗦一下常見的進程內執行緒同步技術:

① lock(Monitor)

開發者最常用的lock關鍵字,使用方式相當簡單,對於單進程內執行緒同步相當有效,實際上lock是Monitor的語法糖,實際的編譯程式碼如下:

object __lockObj = x;  bool __lockWasTaken = false;  try  {       System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);       // Your code...  }  finally  {      if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);  }

一般使用私有靜態對象作為lock(Monitor)執行緒同步的同步對象,那配合lock完成程式碼鎖定的那個對象到底起什麼作用呢?

這裡面有個SyncBlockIndex的概念,新建的託管堆在記憶體表現如下:

每個堆對象:函數表指針(這也是一個重要知識點,用於在多態中判斷對象到底是哪個類型)、同步塊索引、對象欄位;其中同步塊索引是lock解決執行緒同步的關鍵,SyncBlockIndex是一個地址指針(傳送門);

新創建的對象objLock,其SyncBlockindex =-1,不指向任何有效同步塊;

調用靜態類Monitor.Enter(objLock), CRL會尋找一個空閑SyncBlock並將objLock對象的SyncBlockIndex指向該塊, 例如上圖中ObjectA,ObjectC的SyncBlockIndex指向了2個SyncBlock;

Exit(objLock)會將objLock對象的SyncBlockIndex重新賦為 -1, 釋放出來的SyncBlock可以在將來被其他對象SyncBlockIndex引用。

② lock(Monitor) vs SemaphoreSlim

兩者都是進程內執行緒同步技術,SemaphoreSlim訊號量支援多執行緒進入;另外SemaphoreSlim 有非同步等待方法,支援在非同步程式碼中執行緒同步,解決在async code中無法使用lock語法糖的問題

// 實例化單訊號量  static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1,1);    // 非同步等待進入訊號量,如果沒有執行緒被授予對訊號量的訪問許可權,則進入執行保護程式碼;否則此執行緒將在此處等待,直到訊號量被釋放為止  await semaphoreSlim.WaitAsync();  try  {      await Task.Delay(1000);  }  finally  {      // 任務準備就緒後,釋放訊號燈。【準備就緒時始終釋放訊號量】至關重要,否則我們將獲得永遠被鎖定的訊號量      semaphoreSlim.Release();  }

總結

從宏觀上掌握Monitor,Mutex,SemaphoreSlim,Semaphore的區別有利於形成【執行緒同步知識體系】;文章著重記錄進程內執行緒同步技術。