在多線程環境中,多個線程可能會同時訪問同一個資源,爲了不訪問發生衝突,能夠根據訪問的複雜程度採起不一樣的措施算法
原子操做適用於簡單的單個操做,無鎖算法適用於相對簡單的一連串操做,而線程鎖適用於複雜的一連串操做安全
修改狀態要麼成功且狀態改變,要麼失敗且狀態不變,而且外部只能觀察到修改前或者修改後的狀態,修改中途的狀態不能被觀察到多線程
.NET 中,System.Threading.Interlocked 類提供了用於執行原子操做的函數,這些函數接收引用參數(ref),也就是變量的內存地址,而後針對該內存地址中的值執行原子操做函數
不使用線程鎖,經過修改操做的內容使它們知足原子操做的條件性能
.NET 提供了一些線程安全的數據類型,這些數據類型大量應用了無鎖算法來提高訪問速度(在部分狀況下仍須要線程鎖):ui
System.Collections.Consurrent.CurrentBag操作系統
System.Collections.Consurrent.CurrentDictionary<TKey, TValue>線程
System.Collections.Consurrent.CurrentQueue對象
System.Collections.Consurrent.CurrentStack遞歸
有獲取鎖(Acquire)和釋放鎖(Release)兩個操做,在獲取鎖以後和釋放鎖以前進行的操做保證在同一個時間只有一個線程執行,操做內容無需改變,因此線程鎖具備很強的通用性
線程鎖有不一樣的種類,下面將分別介紹自旋鎖,互斥鎖,混合鎖,讀寫鎖
自旋鎖(Spinlock)是最簡單的線程鎖,基於原子操做實現
它使用一個數值來表示鎖是否已經被獲取,0表示未被獲取,1表示已經獲取
獲取鎖時會先使用原子操做設置數值爲1,而後檢查修改前的值是否爲0,若是爲0則表明獲取成功,不然繼續重試直到成功爲止
釋放鎖時會設置數值爲0,其餘正在獲取鎖的線程會在下一次重試時成功獲取
使用原子操做的緣由是,它能夠保證多個線程同時把數值0修改到1時,只有一個線程能夠觀察到修改前的值爲0,其餘線程觀察到修改前的值爲1
.NET 可使用如下的類實現自旋鎖:
System.Threading.Thread.SpinWait
System.Threading.SpinWait
System.Threading.SpinLock
使用自旋鎖有個須要注意的問題,自旋鎖保護的代碼應該在很是短的時間內執行完畢,若是代碼長時間運行則其餘須要獲取鎖的線程會不斷重試並佔用邏輯核心,影響其餘線程運行
此外,若是 CPU 只有一個邏輯核心,自旋鎖在獲取失敗時應該馬上調用 Thread.Yield 函數提示操做系統切換到其餘線程,由於一個邏輯核心同一時間只能運行一個線程,在切換線程以前其餘線程沒有機會運行,也就是切換線程以前自旋鎖沒有機會被釋放
因爲自旋鎖不適用於長時間運行,它的使用場景比較有限,更通用的線程鎖是操做系統提供的基於原子操做與線程調度實現的互斥鎖(Mutex)
與自旋鎖同樣,操做系統提供的互斥鎖內部有一個數值表示是否已經被獲取,不一樣的是當獲取鎖失敗時,它不會反覆重試,而是安排獲取鎖的線程進入等待狀態,並把線程對象添加到鎖關聯的隊列中,另外一個線程釋放鎖時會檢查隊列中是否有線程對象,若是有則通知操做系統喚醒該線程
由於處於等待狀態的線程沒有運行,即便長時間不釋放也不會消耗 CPU 資源,但讓線程進入等待狀態與從等待狀態喚醒並調度運行可能會花費毫秒級的時間,與自旋鎖重試所需的納秒級時間相比很是的長
.NET 提供了 System.Threading.Mutex 類,這個類包裝了操做系統提供的互斥鎖,它是可重入的,已經獲取鎖的線程能夠再次執行獲取蘇鎖的操做,但釋放鎖的操做也要執行相同的次數,可重入的鎖又叫遞歸鎖(Recursive Lock)
遞歸鎖內部使用一個計數器記錄進入次數,同一個線程每獲取一次就加1,釋放一次就減1,減1後若是計數器變爲0就執行真正的釋放操做,通常用在嵌套調用的多個函數中
Mutex 類的另外一個特色是支持跨進程使用,建立時經過構造函數的第二個參數能夠傳入名稱
若是一個進程獲取了鎖,那麼在釋放該鎖前的另外一個進程獲取一樣名稱的鎖須要等待;若是進程獲取了鎖,可是在退出以前沒有調用釋放鎖的方法,那麼鎖會被操做系統自動釋放,其餘當前正在等待鎖(鎖被自動釋放前進入等待狀態)的進程會收到 AbandonedMutexException 異常
跨進程鎖一般用於保護多個進程共享的資源或者防止程序多重啓動
互斥鎖 Mutex 使用時必須建立改類型的實例,由於實例包含了非託管的互斥鎖對象,開發者必須在不使用鎖後儘快調用 Dispose 函數釋放非託管資源,而且由於獲取鎖失敗後會馬上安排線程進入等待,整體上性能比較低
.NET 提供了更通用並且更高性能的混合鎖(Monitor),任何引用類型的對象均可以做爲鎖對象,不須要事先建立指定類型的實例,而且涉及的非託管資源由 .NET 運行時自動釋放,不須要手動調用釋放函數
獲取和釋放混合鎖須要使用 System.Threading.Monitor 類中的函數
C# 提供了 lock 語句來簡化經過 Monitor 類獲取和釋放的代碼
混合鎖的特徵是在獲取鎖失敗後像自旋鎖同樣重試必定的次數,超過必定次數以後(.NET Core 2.1 是30次)再安排當前進程進入等待狀態
混合鎖的好處是,若是第一次獲取鎖失敗,但其餘線程立刻釋放了鎖,當前線程在下一輪重試能夠獲取成功,不須要執行毫秒級的線程調度處理;而若是其餘線程在短期內沒有釋放鎖,線程會在超太重試次數以後進入等待狀態,以免消耗 CPU 資源,所以混合鎖適用於大部分場景
讀寫鎖(ReaderWriterLock)是一個具備特殊用途的線程鎖,適用於頻繁讀取且讀取須要必定時間的場景
共享資源的讀取操做一般是能夠同時執行的,而普通的互斥鎖不論是讀取仍是修改操做都沒法同時執行,若是多個線程爲了讀取操做而獲取互斥鎖,那麼同一時間只有一個線程能夠執行讀取操做,在頻繁讀取的場景下會對吞吐量形成影響
讀寫鎖分爲讀取鎖和寫入鎖,線程能夠根據對共享資源的操做類型選擇獲取讀寫鎖仍是寫入鎖,讀取鎖能夠被多個線程同時獲取,寫入鎖不能夠被多個線程同時獲取,並且讀取鎖和寫入鎖不能夠被不一樣的線程同時獲取
.NET 提供的 System.Threading.ReaderWriterLockSlim 類實現了讀寫鎖,
讀寫鎖也是一個混合鎖(Hybird Lock),在獲取鎖時經過自旋重試必定的次數再進入等待狀態
此外,它還支持同一個線程先獲取讀寫鎖,而後再升級爲寫入鎖,適用於「須要先獲取讀寫鎖,而後讀取共享數據判斷是否須要修改,須要修改時再獲取寫入鎖」的場景
《.NET Core 底層入門》
若是閱讀文章後有所收穫,但願您能夠點一個推薦,感受不盡!!!