合集:.NET Core多線程溫故知新
- .NET Core多線程(1)Thread與Task
- .NET Core多線程(2)異步 - 上
- .NET Core多線程(3)異步 - 下
- .NET Core多線程(4)鎖機制
- .NET Core多線程(5)常見性能問題
去年換工作時系統複習了一下.NET Core多線程相關專題,學習了一線碼農老哥的《.NET 5多線程編程實戰》課程,我將複習的知識進行了總結形成本專題。
本篇,我們來複習一下.NET中鎖機制的相關知識點,預計閱讀時間10分鐘。
理解lock鎖的底層原理
(1)爲什麼要用鎖?
對某個共享代碼區域(臨界區)進行串行訪問,使用lock來保證串行的安全。
(2)lock的用法
lock (lockMe) { dict.Add(i.ToString(), DateTime.Now); }
(3)lock的本質
通過ILSpy反編譯查看可以知道,lock是個語法糖,編譯後其實是Monitor.Enter 和 Monitor.Exit 的封裝。
try { Monitor.Enter(lockMe, ref lockTake); dict.Add(i.ToString(), DateTime.Now); } finally { if (lockTake) { Monitor.Exit(lockMe); } }
(4)lock爲何需要引用類型?
首先,編譯器要求lock中的所對象必須是引用類型。
其次,因爲lock會用到對象頭中的同步塊索引來進行同步,值類型沒有堆中的數據。
無鎖化:線程的本地存儲
(1)線程本地存儲
static 的作用域在AppDomain下都可見,此時在多線程環境中,通過static共享變量的方式來同步,不可避免會出現鎖競爭。如果能將作用域範圍縮小,比如縮小到Thread級別,就可以避免鎖競爭。例如:ConcurrentBag就是一個好的例子。
(2).NET中的解決方案
ThreadStatic(Attribute):當前線程拿到的是定義好的值,其他線程拿到的可能是默認值(值類型可能是0,引用類型可能是null,需要注意容錯)。
ThreadLocal:與ThreadStatic最大的區別在於ThreadStatic只在第一個線程初始化,ThreadLocal則會爲每個線程初始化。
(3)存儲在哪裏?
- PEB 進程環境塊
- TEB 線程環境塊
- TLS 線程本地存儲(Thread Local Storage),取決於一共有多少個DataSlot
(4)應用場景
用來做數據庫連接池:DB連接池 基於 ThreadLocal實現,每個線程只能看見自己的請求隊列;
用來做鏈式追蹤:比如Skywalking或Zipkin等,用到ThreadLocal做本地存儲,記錄完整的調用鏈條如:A -> B -> C -> D;
內核態鎖知多少
(1)基於WaitHandle的內核鎖
這種鎖是基於Windows底層的內核數據結構來維護線程之間的同步,比如:
-
AutoResetEvent / ManualResetEvent
-
Semaphore
-
Mutex
(2)優缺點
需要從用戶態切換到內核態,相對來說比較重量級,相對耗費時間;內核模式的鎖,不僅可用於創建線程同步,還可以創建進程同步。
用戶態鎖知多少
(1)用戶態鎖是啥?
例如下面的代碼:
lock(obj) { ... // todo [1ms] }
大部分都是在臨界區進行等待時間很短(比如1ms)的加鎖,能不能讓thread在CLR或C#層面內旋(自旋)一下,從而提高性能呢?使用用戶態鎖就可以避免上下文切換和內核切換帶來的高開銷。
(2)尋找解決方案
保持線程在用戶態又要儘可能少的消耗CPU時間
時間片
- Windows中一個時間片大概是30ms
- Thread.Sleep(0)
- 提前結束自己的時間片,然後把自己放入到就緒隊列中,如果就緒隊列中的線程優先級 >= Current Thread,那麼其他線程會被調度
- 如果就緒隊列中的線程優先級 < Current Thread,那麼Current Thread只能繼續執行【低優先級線程得不到執行】
- 整體CPU級別
- Thread.Yield()
- 提前結束自己的時間片,如果當前邏輯CPU上的就緒隊列上有待執行的線程,那麼這個線程就會被調度(不考慮優先級)【低優先級線程可以得到執行】
- 邏輯CPU級別
極端休眠時間
- Sleep(1)
- 本質上和Sleep(1000)一樣,都需要休眠
CAS原語
- read, operate, write => 打包成原子性
藉助CLR內的AwareLock::SpinWait()
- C# SpinWait
- CLR SpinWait
(3).NET內置的SpinLock(用戶態)
SpinLock在用法上和lock關鍵字差不多的。
class Program { public static SpinLock spinLock = new SpinLock(); public static int counter = 0; static void Main(string[] args) { Parallel.For(1, 1000001, (i) => { var lockTaken = false; spinLock.Enter(ref lockTaken); ++counter; spinLock.Exit(); } }); Console.WriteLine($"counter={counter}"); Console.ReadLine(); }
(4).NET CAS案例:Interlocked
CPU直接操作的,主要用在一些簡單類型上:
-
read
-
operation
-
write
class Program { public static SpinLock spinLock = new SpinLock(); public static int counter = 0; static void Main(string[] args) { Parallel.For(1, 1000001, (i) => { Interlocked.Increment(ref counter, 1); }); Console.WriteLine($"counter={counter}"); Console.ReadLine(); }
混合態鎖知多少
混合鎖:用戶態模式+內核態模式
(1)ManualResetEventSlim
它是如何實現的?
- lock
- ManualResetEvent
- CAS
- SpinWait(輕量級自旋鎖)、SpinLock
(2)SemaphoreSlim
它是如何實現的?
- ManualResetEvent + lock + SpinWait
(3)ReaderWriterLockSlim
這個鎖的內核版是 ReaderWriterLock,不帶Slim就代表是內核態的鎖。
這個鎖顧名思義是讀寫鎖,意思是:讀可以並行,但寫只能串行。EnterWriteLock() 需要等待所有的reader或writer鎖結束,才能開始
(4)CountdownEvent
這個鎖可以實現類似MapReduce的效果。
它是如何實現的?
基於ManualResetEvent事件做了底層封裝。
線程安全集合知多少
(1)線程安全集合
.NET中都有哪些線程安全的集合類型?
ConcurrentBag 對應非線程安全類型:List
ConcurrentQueue 對應非線程安全類型:Queue
ConcurrentStack 對應非線程安全類型:Stack
ConcurrentDictionary 對應非線程安全類型:Dictionary
(2)BlockingCollection
BlockingCollection 意爲 阻塞集合。
線程安全的集合 可以轉換爲 阻塞集合,只要它實現了IProducerConsumerCollection接口BlockingCollection可以實現類似發佈訂閱的業務場景應用:
-
生產端Add進去發佈的消息
-
消費者端通過GetConsumingEnumerable()方法阻塞等待發布的消息
ConcurrentDictonary的兩個大坑
(1)Values的坑
-
觀察現象
-
業務場景:自己用ConcurrentDictionary封裝了一個Cache
-
FullGC 將 LOH 上的對象回收了
-
所有>=85000byte的都會被納入LOH
-
觀察源碼
-
Values方法每次都會生成一個新的List集合對象進行返回,每個對象都是大對象
-
如何改進
-
禁止調用Values方法
-
藉助lock + Dictionary實現類似操作避免每次生成新的List集合對象
(2)GetOrAdd的坑
-
觀察現象
-
業務場景:自己用ConcurrentDictionary封裝了一個Redis連接池緩存
-
藉助GetOrAdd實現的CreateInstance方法未能實現線程安全導致連接池被大量反覆創建
-
觀察源碼
-
GetOrAdd方法中的valueFactory不是線程安全的
-
如何改進
-
藉助Lazy改造字典的Value對象,保證創建方法只被執行一次,比如:將RedisConnection改爲Lazy
共享變量在Release模式下的Bug
(1)現象
同樣的代碼,通過共享變量控制工作線程是否要結束自己,在Debug模式下沒有問題,但是在Release模式下有問題。
(2)原因
JIT提供了錯誤的決策導致CPU在解析代碼時做了優化,將 共享變量 存放在了CPU的寄存器中。
(3)WinDbg探究
-
Release模式
-
查看memory中的共享變量的值
-
CPU寄存器
-
查看共享變量的值
(4)解決方案
-
使用CancellationToken做取消
-
不用Cache,都讀內存address中的對象,性能會相對較低
-
將共享變量 改爲 易變結構,比如:private bool _shouldStop 改爲 private volatile bool _shouldStop
小結
本篇,我們複習了鎖機制相關的知識點。下一篇,我們將複習一下常見的.NET多線程相關的性能優化實踐。
參考資料
一線碼農,騰訊課堂《.NET 5多線程編程實戰》
不明作者,《Task調度與await》