C#開發者(面試者)都會遇到lock(Monitor),Mutex,Semaphore,SemaphoreSlim這四個與鎖相關的C#類型,本文期望以最簡潔明了的方式闡述四種對象的區別。
什么叫線程安全?
教條式理解
如果代碼在多線程環境中運行的結果與 單線程運行結果一樣,其他變量值也和預期是一樣的,那么線程就是安全的;
線程不安全就是不提供數據訪問保護,可能出現多個線程先后修改數據造成的結果是臟數據。
實際場景理解
兩個線程都為集合增加元素,我們錯誤的理解即使是多線程也總有先后順序吧,集合的兩個位置先后塞進去就完了;實際上集合增加元素這個行為看起來簡單,實際並不一定是原子操作。
在添加一個元素的時候,它可能會有兩步來完成:
- 在 Items[Size] 的位置存放此元素;
- 增大 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的區別有利於形成【線程同步知識體系】;
文章着重記錄 進程內線程同步技術。