CPU原子操作
原子操作,指一段邏輯要么全部成功,要么全部失敗。概念上類似數據庫事務(Transaction).
CPU能夠保證單條匯編的原子性,但不保證多條匯編的原子性
那么在這種情況下,那么CPU如何保證原子性呢?CPU本身也有鎖機制,從而實現原子操作
眼見為實
int location = 10;
location++;
Interlocked.Increment(ref location);
常規代碼不保證原子性
使用Interlocked類,底層使用CPU鎖來保證原子性
CPU lock前綴保證了操作同一內存地址的指令不能在多個邏輯核心上同時執行
8byte數據在32位架構的尷尬
思考一個問題,x86架構(注意不是x86-64)的位寬是32位,寄存器一次性最多只能讀取4byte數據,那么面對long類型的數據,它能否保證原子性呢?
可以看到,x86架構面對8byte數據,分為了兩步走,首先將低8位FFFFFFFF賦值給exa寄存器,再將高8位的7FFFFFFF賦值給edx寄存器。最后再拼接起來。而面對4byte的int,則一步到位。
前面說到,多條匯編CPU不保證原子性,因此當x86架構面對超過4byte的數據不保證原子性。
如何解決?
要解決此類尷尬情況,要么使用64位架構,要么利用CPU的鎖機制來保證原子性。
C#中的Interlocked.Read為了解決long類型的尷尬而生,是一個利用CPU鎖很好的例子
用戶態鎖
操作系統中鎖分為兩種,用戶態鎖(user-mode)和內核態鎖(kernel-mode)。
優點:
通常性能較高,因為不需要進行用戶態與內核態的切換,避免了切換帶來的額外開銷,如上下文保存與恢復等。例如在無競爭的情況下,用戶態的自旋鎖和互斥鎖都可以快速地獲取和釋放鎖,執行時間相對較短.
缺點:
在高并發競爭激烈的情況下,如果線程長時間獲取不到鎖,自旋鎖會導致 CPU 空轉浪費資源,而互斥鎖的等待隊列管理等也會在用戶態消耗一定的 CPU 時間.
Volatile
在 C# 中,volatile是一個關鍵字,用于修飾字段。它告訴編譯器和運行時環境,被修飾的字段可能會被多個線程同時訪問,并且這些訪問可能是異步的。這意味著編譯器不能對該字段進行某些優化,以確保在多線程環境下能夠正確地讀取和寫入這個字段的值
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編譯優化的時候,發現while (!s_stopWorker)中的s_stopWorker在該方法中永遠不會變。因此就自作主張直接生成了while(ture)來“優化”代碼
class MyClass{
private int _myField;
public void MyMethod()
{
_myField = 5;
int localVar = _myField;
}
}
編譯器認為_myField被賦值5后,不會被其它線程改變。所有它會_myFieId的值直接加載到寄存器中,而后續使用localVar時,直接從寄存器讀取(CPU緩存),而不是再次從內存中讀取。這種優化在單線程中是沒有問題的,但在多線程環境下,會存在問題。
因此我們需要在變量前,加volatile關鍵字。來告訴編譯器不要優化。
自旋鎖
使用Interlocked實現一個最簡單的自旋鎖
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優化
上面的自旋鎖有一個很大問題,就是CPU會全力運算。使用CPU最大的性能。
實際上,當我們沒有獲取到鎖的時候,完全可以讓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函數在x86平臺上會調用pause指令,pause指令實現一個很短的延遲空等待操作,這個指令的執行時間大概是10+個 CPU時鐘周期。讓CPU跑得慢一點。
使用SpinWait優化
Thread.SpinWait本質上是讓CPU偷懶跑得慢一點,最多降低點功耗。并沒有讓出CPU時間片,所以治標不治本
因此可以使用SpinWait來進一步優化。
可以看到,在合適的情況下。SpinWait會讓出當前時間片,以此提高執行效率。比Thread.SpinWait占著資源啥也不做強不少
使用SpinLock代替
SpinLock是C#提供的一種自旋鎖,封裝了管理鎖狀態和SpinWait.SpinOnce方法的邏輯,雖然做的事情相同,但是代碼更健壯也更容易理解
其底層還是使用的SpinWait
內核態鎖
優點:
內核態鎖由操作系統內核管理和調度,當鎖被釋放時,內核可以及時地喚醒等待的線程,適用于復雜的同步場景和長時間等待的情況.
缺點:
由于涉及到用戶態與內核態的切換,開銷較大,這在鎖的競爭不激烈或者臨界區執行時間較短時,會對性能產生較大的影響
事件(ManualResetEvent/AutoResetEvent)與信號量(Semaphores)是Windows內核中兩種基元線程同步鎖,其它內核鎖都是在它們基礎上的封裝
Event鎖
Event鎖有兩種,分為ManualResetEvent\AutoResetEvent 。本質上是由內核維護的Int64變量當作bool來使,標識0/1兩種狀態,再根據狀態決定線程等待與否。
需要注意的是,等待不是原地自旋,并不會浪費CPU性能。而是會放入CPU _KPRCB結構的WaitListHead鏈表中,不執行任何操作。等待系統喚醒
線程進入等待狀態與喚醒可能會花費毫秒級,與自旋的納秒相比,時間非常長。所以適合鎖競爭非常激烈的場景
眼見為實:是否調用了win32 API(進入內核態)?
在Windows上Event對象通過CreateEventEx函數來創建,狀態變化使用Win32 API ResetEvent/SetEvent
眼見為實:內核態中是否真的有long變量來維護狀態?
https://github.com/reactos/reactos/blob/master/sdk/include/xdk/ketypes.h
底層使用SignalState來存儲狀態
Semaphore鎖
Semaphore的本質是由內核維護的Int64變量,信號量為0時,線程等待。信號量大于0時,解除等待。
它相對Event鎖來說,比較特殊點是內部使用一個int64來記錄數量(limit),舉個例子,Event鎖管理的是一把椅子是否被坐下,表狀態。而Semaphore管理的是100把椅子中,有多少坐下,有多少沒坐下,表臨界點。擁有更多的靈活性。
眼見為實:是否調用了win32 API(進入內核態)?
在Windows上信號量對象通過CreateSemaphoreEx函數來創建,增加信號量使用ReleaseSemaphore,減少信號量使用WaitForMultipleObject
眼見為實:內核態中是否真的有long變量來維護狀態?
參考Event鎖,它們內部共享同一個結構
Mutex鎖
Mutex是Event與Semaphore的封裝,不做過多解讀。
眼見為實:是否調用了win32 API(進入內核態)?
在Windows上,互斥鎖通過CreateMutexEx函數來創建,獲取鎖用WaitForMultipleObjectsEx,釋放鎖用ReleaseMutex
混合鎖
用戶態鎖有它的好,內核鎖有它的好。把兩者合二為一有沒有搞頭呢?
混合鎖是一種結合了自旋鎖和內核鎖的鎖機制,在不同的情況下使用不同策略,明顯是一種更好的類型。
Lock
Lock是一個非常經典且常用的混合鎖,其內部由兩部分構成,也分別對應不同場景下的用戶態與內核態實現
自旋鎖(Thinlock):CoreCLR中別名瘦鎖
內核鎖(AwareLock):CoreClr中別名AwareLock,其底層是AutoResetEvent實現
Lock鎖先使用用戶態鎖自旋一定次數,如果獲取不到鎖。再轉換成內核態鎖。從而降低CPU消耗。
Lock鎖原理
Lock鎖的原理是在對象的ObjectHeader上存放一個線程Id,當其它鎖要獲取這個對象的鎖時,看一下有沒有存放線程Id,如果有值,說明還被其他鎖持有,那么當前線程則會短暫性自旋,如果在自旋期間能夠拿到鎖,那么鎖的性能將會非常高。如果自旋一定次數后,沒有拿到鎖,鎖就會退化為內核鎖。
現在你理解了,為什么lock一定要鎖一個引用類型吧?
點擊查看代碼
眼見為實:在自旋失敗后,退化為內核鎖
點擊查看代碼
首先自旋,然后自旋失敗,轉成內核鎖,并用SyncBlock 來維護鎖相關的統計信息,01代表SyncBlock的Index,08是一個常量,代表內核鎖
其它混合鎖
基本上以Slim結尾的鎖,都是混合鎖。都是先自旋一定次數,再進入內核態。
比如ReaderWriterSlim,SemaphoreSlim,ManualResetEventSlim.
異步鎖
在C#中,SemaphoreSlim可以在一定程度上用于異步場景。它可以限制同時訪問某個資源的異步操作的數量。例如,在一個異步的 Web 請求處理場景中,可以使用SemaphoreSlim來控制同時處理請求的數量。然而,它并不能完全替代真正的異步鎖,因為它主要是控制并發訪問的數量,而不是像傳統鎖那樣提供互斥訪問
Nito.AsyncEx 介紹
https://github.com/StephenCleary/AsyncEx
大神維護了的一個異步鎖的開源庫,它將同步版的鎖結構都做了一份異步版,彌補了.NET框架中的對異步鎖支持不足的遺憾
無鎖算法
即使是最快的鎖,也數倍慢于沒有鎖的代碼,因從CAS無鎖算法應運而生。
無鎖算法大量依賴原子操作,如比較并交換(CAS,Compare - And - Swap)、加載鏈接 / 存儲條件(LL/SC,Load - Linked/Store - Conditional)等。以 CAS 為例,它是一種原子操作,用于比較一個內存位置的值與預期值,如果相同,就將該位置的值更新為新的值。
舉個例子
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;
}
}
無鎖算法的優缺點
上面提到的無鎖算法不一定比使用線程快。比如
每次都要New對象分配內存,這個取決于你的業務復雜度。
如果Interlocked.CompareExchange一直交換失敗,會類似自旋鎖一樣大量占用CPU資源
簡單匯總一下
優點:
缺點:
實現復雜:無鎖算法的設計和實現相對復雜,需要深入理解底層的原子操作、內存模型和并發編程原理。錯誤的實現可能會導致數據不一致、死鎖或者活鎖等問題。
ABA 問題:這是無鎖算法中常見的一個問題。例如在使用 CAS 操作時,一個內存位置的值從 A 變為 B,然后又變回 A,這可能會導致一些無鎖算法誤判。解決 ABA 問題通常需要額外的標記或者版本號機制來記錄內存位置的變化歷史。
內存順序問題:在多核處理器環境下,由于處理器緩存和指令重排等因素,無鎖算法需要考慮內存順序問題,以確保不同線程對共享數據結構的操作順序符合預期,避免出現數據不一致的情況。這通常需要使用內存屏障等技術來輔助解決。
?轉自https://www.cnblogs.com/lmy5215006/p/18585588
該文章在 2024/12/6 9:24:21 編輯過