一文看懂.Net線程同步技術 lock(Monitor) vs Mutex vs Semaphore vs SemaphoreSlim

  • 2019 年 10 月 30 日
  • 筆記

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. 我們常用的是HttpContext.Current

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

C#線程同步技術

話不多說, 給出大圖:

四象限對象的區別:

該線程同步技術

  -  支持線程進入的個數

       –  是否跨進程支持 

其中

 ① lock  vs Monitor

最常用的lock關鍵字,能在多線程環境下確保只有一個線程在執行 {被保護的代碼},其他線程則必須等待進入的線程完成工作代碼。

上圖將lock和Monitor放在一起,是因為lock是Monitor的語法糖,實際的編譯代碼如下:

bool lockTaken = false;  try  {    Monitor.Enter(obj, ref lockTaken);    //...  }  finally  {    if (lockTaken) Monitor.Exit(obj);  }

 ② lock(Monitor)vs Mutex(中文稱為互斥鎖,互斥元)

lock/Monitor 維護進程內線程的安全性,Mutex 維護跨進程的線程安全性。

這2個對象都只支持單線程進入指定代碼。

 ③ SemaphoreSlim  vs Semaphore 

 中文都稱為信號量,根據對象初始化的配置,能夠允許單個或多個線程進入保護代碼。

信號量使多個並發線程可以訪問共享資源(最大為您指定的最大數量),當線程請求訪問資源時,信號量計數遞減,而當它們釋放資源時,信號量計數又遞增。

SemaphoreSlim 是一個輕量級的,由CRL支持的進程內信號量。

 右側Mutex 和Semaphore 都是內核對象,可以看到他們都繼承自WaitHandle對象,

 左側Monitor,SemaphoreSlim是.NET CLR對象,

④ Monitor  vs SemaphoreSlim

  兩者都是進程內線程同步技術,SemaphoreSlim信號量支持多線程進入;

 另外SemaphoreSlim 有異步等待方法,支持在異步代碼中線程同步, 能解決在async code中無法使用lock語法糖的問題;

// 實例化單信號量  static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1,1);    // 異步等待進入信號量,如果沒有線程被授予對信號量的訪問權限,則進入執行保護代碼;否則此線程將在此處等待,直到信號量被釋放為止  await semaphoreSlim.WaitAsync();  try  {      await Task.Delay(1000);  }  finally  {      // 任務準備就緒後,釋放信號燈。【準備就緒時始終釋放信號量】至關重要,否則我們將獲得永遠被鎖定的信號量
// 這就是為什麼在try ... finally子句中進行發佈很重要的原因;程序執行可能會崩潰或採用其他路徑,這樣可以保證執行 semaphoreSlim.Release(); }

從目前看跨進程線程同步很少見,倒是分佈式鎖越來越多常見了。

總結:

文章沒有講述每個對象使用方式,自行看MSDN doc。從象限圖中快速知曉 這4種線程同步技術的區別:

–   是否支持跨進程線程同步

–   是否支持多線程進入被保護代碼。

從宏觀上了解四個對象的區別對於【線程同步】知識體系的形成是有幫助的。