CPU原子操作
原子操作,指一段邏輯要么全部成功,要么全部失敗。概念上類似數(shù)據(jù)庫事務(wù)(Transaction).
CPU能夠保證單條匯編的原子性,但不保證多條匯編的原子性
那么在這種情況下,那么CPU如何保證原子性呢?CPU本身也有鎖機(jī)制,從而實(shí)現(xiàn)原子操作
眼見為實(shí)
int location = 10;
location++;
Interlocked.Increment(ref location);

常規(guī)代碼不保證原子性

使用Interlocked類,底層使用CPU鎖來保證原子性
CPU lock前綴保證了操作同一內(nèi)存地址的指令不能在多個邏輯核心上同時執(zhí)行
8byte數(shù)據(jù)在32位架構(gòu)的尷尬
思考一個問題,x86架構(gòu)(注意不是x86-64)的位寬是32位,寄存器一次性最多只能讀取4byte數(shù)據(jù),那么面對long類型的數(shù)據(jù),它能否保證原子性呢?

可以看到,x86架構(gòu)面對8byte數(shù)據(jù),分為了兩步走,首先將低8位FFFFFFFF賦值給exa寄存器,再將高8位的7FFFFFFF賦值給edx寄存器。最后再拼接起來。而面對4byte的int,則一步到位。
前面說到,多條匯編CPU不保證原子性,因此當(dāng)x86架構(gòu)面對超過4byte的數(shù)據(jù)不保證原子性。
如何解決?
要解決此類尷尬情況,要么使用64位架構(gòu),要么利用CPU的鎖機(jī)制來保證原子性。
C#中的Interlocked.Read為了解決long類型的尷尬而生,是一個利用CPU鎖很好的例子

用戶態(tài)鎖
操作系統(tǒng)中鎖分為兩種,用戶態(tài)鎖(user-mode)和內(nèi)核態(tài)鎖(kernel-mode)。
優(yōu)點(diǎn):
通常性能較高,因?yàn)椴恍枰M(jìn)行用戶態(tài)與內(nèi)核態(tài)的切換,避免了切換帶來的額外開銷,如上下文保存與恢復(fù)等。例如在無競爭的情況下,用戶態(tài)的自旋鎖和互斥鎖都可以快速地獲取和釋放鎖,執(zhí)行時間相對較短.
缺點(diǎn):
在高并發(fā)競爭激烈的情況下,如果線程長時間獲取不到鎖,自旋鎖會導(dǎo)致 CPU 空轉(zhuǎn)浪費(fèi)資源,而互斥鎖的等待隊(duì)列管理等也會在用戶態(tài)消耗一定的 CPU 時間.
Volatile
在 C# 中,volatile是一個關(guān)鍵字,用于修飾字段。它告訴編譯器和運(yùn)行時環(huán)境,被修飾的字段可能會被多個線程同時訪問,并且這些訪問可能是異步的。這意味著編譯器不能對該字段進(jìn)行某些優(yōu)化,以確保在多線程環(huán)境下能夠正確地讀取和寫入這個字段的值
static class StrangeBehavior
{
private static bool s_stopWorker = false;
public static void Run()
{
Thread t = new Thread(Worker);
t.Start();
Thread.Sleep(5000);
s_stopWorker = true;
}
private static void Worker()
{
int x = 0;
while (!s_stopWorker)
{
x++;
}
Console.WriteLine($"worker:stopped when x={x}");
}
}
JIT編譯優(yōu)化的時候,發(fā)現(xiàn)while (!s_stopWorker)中的s_stopWorker在該方法中永遠(yuǎn)不會變。因此就自作主張直接生成了while(ture)來“優(yōu)化”代碼
class MyClass{
private int _myField;
public void MyMethod()
{
_myField = 5;
int localVar = _myField;
}
}
編譯器認(rèn)為_myField被賦值5后,不會被其它線程改變。所有它會_myFieId的值直接加載到寄存器中,而后續(xù)使用localVar時,直接從寄存器讀取(CPU緩存),而不是再次從內(nèi)存中讀取。這種優(yōu)化在單線程中是沒有問題的,但在多線程環(huán)境下,會存在問題。
因此我們需要在變量前,加volatile關(guān)鍵字。來告訴編譯器不要優(yōu)化。
自旋鎖
使用Interlocked實(shí)現(xiàn)一個最簡單的自旋鎖
public struct SpinLockSmiple
{
private int _useNum = 0;
public SpinLockSmiple()
{
}
public void Enter()
{
while (true)
{
if (Interlocked.Exchange(ref _useNum, 1) == 0)
return;
}
}
public void Exit()
{
Interlocked.Exchange(ref _useNum, 0);
}
}
使用Thread.SpinWait優(yōu)化
上面的自旋鎖有一個很大問題,就是CPU會全力運(yùn)算。使用CPU最大的性能。
實(shí)際上,當(dāng)我們沒有獲取到鎖的時候,完全可以讓CPU“偷懶”一下
public struct SpinLockSmiple
{
private int _useNum = 0;
public SpinLockSmiple()
{
}
public void Enter()
{
while (true)
{
if (Interlocked.Exchange(ref _useNum, 1) == 0)
return;
Thread.SpinWait(10);;
}
}
public void Exit()
{
Interlocked.Exchange(ref _useNum, 0);
}
}
SpinWait函數(shù)在x86平臺上會調(diào)用pause指令,pause指令實(shí)現(xiàn)一個很短的延遲空等待操作,這個指令的執(zhí)行時間大概是10+個 CPU時鐘周期。讓CPU跑得慢一點(diǎn)。
使用SpinWait優(yōu)化
Thread.SpinWait本質(zhì)上是讓CPU偷懶跑得慢一點(diǎn),最多降低點(diǎn)功耗。并沒有讓出CPU時間片,所以治標(biāo)不治本
因此可以使用SpinWait來進(jìn)一步優(yōu)化。

可以看到,在合適的情況下。SpinWait會讓出當(dāng)前時間片,以此提高執(zhí)行效率。比Thread.SpinWait占著資源啥也不做強(qiáng)不少
使用SpinLock代替
SpinLock是C#提供的一種自旋鎖,封裝了管理鎖狀態(tài)和SpinWait.SpinOnce方法的邏輯,雖然做的事情相同,但是代碼更健壯也更容易理解

其底層還是使用的SpinWait
內(nèi)核態(tài)鎖
優(yōu)點(diǎn):
內(nèi)核態(tài)鎖由操作系統(tǒng)內(nèi)核管理和調(diào)度,當(dāng)鎖被釋放時,內(nèi)核可以及時地喚醒等待的線程,適用于復(fù)雜的同步場景和長時間等待的情況.
缺點(diǎn):
由于涉及到用戶態(tài)與內(nèi)核態(tài)的切換,開銷較大,這在鎖的競爭不激烈或者臨界區(qū)執(zhí)行時間較短時,會對性能產(chǎn)生較大的影響
事件(ManualResetEvent/AutoResetEvent)與信號量(Semaphores)是Windows內(nèi)核中兩種基元線程同步鎖,其它內(nèi)核鎖都是在它們基礎(chǔ)上的封裝

Event鎖
Event鎖有兩種,分為ManualResetEvent\AutoResetEvent 。本質(zhì)上是由內(nèi)核維護(hù)的Int64變量當(dāng)作bool來使,標(biāo)識0/1兩種狀態(tài),再根據(jù)狀態(tài)決定線程等待與否。
需要注意的是,等待不是原地自旋,并不會浪費(fèi)CPU性能。而是會放入CPU _KPRCB結(jié)構(gòu)的WaitListHead鏈表中,不執(zhí)行任何操作。等待系統(tǒng)喚醒
線程進(jìn)入等待狀態(tài)與喚醒可能會花費(fèi)毫秒級,與自旋的納秒相比,時間非常長。所以適合鎖競爭非常激烈的場景
眼見為實(shí):是否調(diào)用了win32 API(進(jìn)入內(nèi)核態(tài))?
在Windows上Event對象通過CreateEventEx函數(shù)來創(chuàng)建,狀態(tài)變化使用Win32 API ResetEvent/SetEvent


眼見為實(shí):內(nèi)核態(tài)中是否真的有l(wèi)ong變量來維護(hù)狀態(tài)?
https://github.com/reactos/reactos/blob/master/sdk/include/xdk/ketypes.h

底層使用SignalState來存儲狀態(tài)
Semaphore鎖
Semaphore的本質(zhì)是由內(nèi)核維護(hù)的Int64變量,信號量為0時,線程等待。信號量大于0時,解除等待。
它相對Event鎖來說,比較特殊點(diǎn)是內(nèi)部使用一個int64來記錄數(shù)量(limit),舉個例子,Event鎖管理的是一把椅子是否被坐下,表狀態(tài)。而Semaphore管理的是100把椅子中,有多少坐下,有多少沒坐下,表臨界點(diǎn)。擁有更多的靈活性。
眼見為實(shí):是否調(diào)用了win32 API(進(jìn)入內(nèi)核態(tài))?
在Windows上信號量對象通過CreateSemaphoreEx函數(shù)來創(chuàng)建,增加信號量使用ReleaseSemaphore,減少信號量使用WaitForMultipleObject


眼見為實(shí):內(nèi)核態(tài)中是否真的有l(wèi)ong變量來維護(hù)狀態(tài)?
參考Event鎖,它們內(nèi)部共享同一個結(jié)構(gòu)

Mutex鎖
Mutex是Event與Semaphore的封裝,不做過多解讀。
眼見為實(shí):是否調(diào)用了win32 API(進(jìn)入內(nèi)核態(tài))?
在Windows上,互斥鎖通過CreateMutexEx函數(shù)來創(chuàng)建,獲取鎖用WaitForMultipleObjectsEx,釋放鎖用ReleaseMutex



混合鎖
用戶態(tài)鎖有它的好,內(nèi)核鎖有它的好。把兩者合二為一有沒有搞頭呢?

混合鎖是一種結(jié)合了自旋鎖和內(nèi)核鎖的鎖機(jī)制,在不同的情況下使用不同策略,明顯是一種更好的類型。
Lock
Lock是一個非常經(jīng)典且常用的混合鎖,其內(nèi)部由兩部分構(gòu)成,也分別對應(yīng)不同場景下的用戶態(tài)與內(nèi)核態(tài)實(shí)現(xiàn)
自旋鎖(Thinlock):CoreCLR中別名瘦鎖
內(nèi)核鎖(AwareLock):CoreClr中別名AwareLock,其底層是AutoResetEvent實(shí)現(xiàn)
Lock鎖先使用用戶態(tài)鎖自旋一定次數(shù),如果獲取不到鎖。再轉(zhuǎn)換成內(nèi)核態(tài)鎖。從而降低CPU消耗。
Lock鎖原理
Lock鎖的原理是在對象的ObjectHeader上存放一個線程Id,當(dāng)其它鎖要獲取這個對象的鎖時,看一下有沒有存放線程Id,如果有值,說明還被其他鎖持有,那么當(dāng)前線程則會短暫性自旋,如果在自旋期間能夠拿到鎖,那么鎖的性能將會非常高。如果自旋一定次數(shù)后,沒有拿到鎖,鎖就會退化為內(nèi)核鎖。
現(xiàn)在你理解了,為什么lock一定要鎖一個引用類型吧?
點(diǎn)擊查看代碼


眼見為實(shí):在自旋失敗后,退化為內(nèi)核鎖
點(diǎn)擊查看代碼


首先自旋,然后自旋失敗,轉(zhuǎn)成內(nèi)核鎖,并用SyncBlock 來維護(hù)鎖相關(guān)的統(tǒng)計(jì)信息,01代表SyncBlock的Index,08是一個常量,代表內(nèi)核鎖


其它混合鎖
基本上以Slim結(jié)尾的鎖,都是混合鎖。都是先自旋一定次數(shù),再進(jìn)入內(nèi)核態(tài)。
比如ReaderWriterSlim,SemaphoreSlim,ManualResetEventSlim.
異步鎖
在C#中,SemaphoreSlim可以在一定程度上用于異步場景。它可以限制同時訪問某個資源的異步操作的數(shù)量。例如,在一個異步的 Web 請求處理場景中,可以使用SemaphoreSlim來控制同時處理請求的數(shù)量。然而,它并不能完全替代真正的異步鎖,因?yàn)樗饕强刂撇l(fā)訪問的數(shù)量,而不是像傳統(tǒng)鎖那樣提供互斥訪問
Nito.AsyncEx 介紹
https://github.com/StephenCleary/AsyncEx
大神維護(hù)了的一個異步鎖的開源庫,它將同步版的鎖結(jié)構(gòu)都做了一份異步版,彌補(bǔ)了.NET框架中的對異步鎖支持不足的遺憾
無鎖算法
即使是最快的鎖,也數(shù)倍慢于沒有鎖的代碼,因從CAS無鎖算法應(yīng)運(yùn)而生。
無鎖算法大量依賴原子操作,如比較并交換(CAS,Compare - And - Swap)、加載鏈接 / 存儲條件(LL/SC,Load - Linked/Store - Conditional)等。以 CAS 為例,它是一種原子操作,用于比較一個內(nèi)存位置的值與預(yù)期值,如果相同,就將該位置的值更新為新的值。
舉個例子
internal class Program{
public static DualCounter Counter = new DualCounter(0, 0);
static void Main(string[] args)
{
Task.Run(IncrementCounters);
Task.Run(IncrementCounters);
Task.Run(IncrementCounters);
Console.ReadLine();
}
public static DualCounter Increment(ref DualCounter counter)
{
DualCounter oldValue, newValue;
do
{
oldValue = counter;
newValue = new DualCounter(oldValue.A + 1, oldValue.B + 1);
}
while (Interlocked.CompareExchange(ref counter, newValue, oldValue) != oldValue);
return newValue;
}
public static void IncrementCounters()
{
var result = Increment(ref Counter);
Console.WriteLine("{0},{1}",result.A,result.B);
}
}public class DualCounter{
public int A { get; }
public int B { get; }
public DualCounter(int a,int b)
{
A = a;
B = b;
}
}
無鎖算法的優(yōu)缺點(diǎn)
上面提到的無鎖算法不一定比使用線程快。比如
每次都要New對象分配內(nèi)存,這個取決于你的業(yè)務(wù)復(fù)雜度。
如果Interlocked.CompareExchange一直交換失敗,會類似自旋鎖一樣大量占用CPU資源
簡單匯總一下
優(yōu)點(diǎn):
高性能:由于避免了鎖的開銷,如線程的阻塞和喚醒、上下文切換等,無鎖算法在高并發(fā)場景下可能具有更好的性能。特別是當(dāng)鎖競爭激烈時,無鎖算法能夠更有效地利用系統(tǒng)資源,減少線程等待時間。
可擴(kuò)展性好:無鎖算法在多核處理器環(huán)境下能夠更好地發(fā)揮多核的優(yōu)勢,因?yàn)槎鄠€線程可以同時對共享數(shù)據(jù)結(jié)構(gòu)進(jìn)行操作,而不受傳統(tǒng)鎖機(jī)制的限制,能夠更好地支持大規(guī)模的并發(fā)訪問。
缺點(diǎn):
實(shí)現(xiàn)復(fù)雜:無鎖算法的設(shè)計(jì)和實(shí)現(xiàn)相對復(fù)雜,需要深入理解底層的原子操作、內(nèi)存模型和并發(fā)編程原理。錯誤的實(shí)現(xiàn)可能會導(dǎo)致數(shù)據(jù)不一致、死鎖或者活鎖等問題。
ABA 問題:這是無鎖算法中常見的一個問題。例如在使用 CAS 操作時,一個內(nèi)存位置的值從 A 變?yōu)?B,然后又變回 A,這可能會導(dǎo)致一些無鎖算法誤判。解決 ABA 問題通常需要額外的標(biāo)記或者版本號機(jī)制來記錄內(nèi)存位置的變化歷史。
內(nèi)存順序問題:在多核處理器環(huán)境下,由于處理器緩存和指令重排等因素,無鎖算法需要考慮內(nèi)存順序問題,以確保不同線程對共享數(shù)據(jù)結(jié)構(gòu)的操作順序符合預(yù)期,避免出現(xiàn)數(shù)據(jù)不一致的情況。這通常需要使用內(nèi)存屏障等技術(shù)來輔助解決。
?轉(zhuǎn)自https://www.cnblogs.com/lmy5215006/p/18585588
該文章在 2024/12/6 9:24:21 編輯過