一文看懂 Mutex vs Semaphore vs Monitor vs SemaphoreSlim

  • 2019 年 10 月 30 日
  • 筆記

C#開發者(面試者)都會遇到Mutex,Semaphore,Monitor,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);    //Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore, code execution will proceed, otherwise this thread waits here until the semaphore is released   await semaphoreSlim.WaitAsync();  try  {      await Task.Delay(1000);  }  finally  {      //When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.      //This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution      semaphoreSlim.Release();  }

總結:

象限圖中快速知曉 這4種執行緒同步技術的區別:

–   是否支援跨進程執行緒同步

–   是否支援多執行緒進入被保護程式碼。