.Net線程同步技術解讀


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(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的區別有利於形成【線程同步知識體系】;

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM