.Net執行緒同步技術解讀
- 2019 年 12 月 5 日
- 筆記
什麼是執行緒安全
教條式理解
如果程式碼在多執行緒環境中運行的結果與單執行緒運行結果一樣,其他變數值也和預期是一樣的,那麼執行緒就是安全的;
結合場景理解
兩個執行緒都為集合增加元素,我們錯誤的理解即使是多執行緒也總有先後順序吧,集合的兩個位置先後塞進去就完了;實際上集合增加元素這個行為看起來簡單,實際並不一定是原子操作。
在添加一個元素的時候,它可能會有兩步來完成:
- 在 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.
各語言推出了適用於不同範圍的執行緒同步技術來預防以上臟數據(實現執行緒安全)
執行緒同步技術

四象限對象的區別:
- 支援執行緒進入的個數
- 是否跨進程支援
上半區 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是一個地址指針(傳送門);
調用靜態類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 { // 任務準備就緒後,釋放訊號燈。【準備就緒時始終釋放訊號量】至關重要,否則我們將獲得永遠被鎖定的訊號量 semaphoreSlim.Release(); }
總結
從宏觀上掌握Monitor,Mutex,SemaphoreSlim,Semaphore的區別有利於形成【執行緒同步知識體系】;文章著重記錄進程內執行緒同步技術。