.Net線程同步技術解讀

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

什麼叫線程安全?

教條式理解

若是代碼在多線程環境中運行的結果與 單線程運行結果同樣,其餘變量值也和預期是同樣的,那麼線程就是安全的;mongodb

線程不安全就是不提供數據訪問保護,可能出現多個線程前後修改數據形成的結果是髒數據。安全

實際場景理解 

兩個線程都爲集合增長元素,咱們錯誤的理解即便是多線程也總有前後順序吧,集合的兩個位置前後塞進去就完了;實際上集合增長元素這個行爲看起來簡單,實際並不必定是原子操做。多線程

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

  1. 在 Items[Size] 的位置存放此元素;
  2. 增大 Size 的值。
  • 在單線程運行的狀況下,若是 Size = 0,添加一個元素後,此元素在位置0,以後設置Size=1;less

  • 若是是在多線程場景下,有兩個線程,線程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.async

    • 咱們對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(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 { // 任務準備就緒後,釋放信號燈。【準備就緒時始終釋放信號量】相當重要,不然咱們將得到永遠被鎖定的信號量
// 這就是爲何在try ... finally子句中進行發佈很重要的緣由;程序執行可能會崩潰或採用其餘路徑,這樣能夠保證執行 semaphoreSlim.Release(); }

總結:

從宏觀上掌握 Monitor,Mutex, SemaphoreSlim,Semaphore的區別有利於造成【線程同步知識體系】;

文章着重記錄 進程內線程同步技術。

相關文章
相關標籤/搜索