狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

.NET Core 鎖(Lock)底層原理淺談

freeflydom
2024年12月6日 9:18 本文熱度 1259

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)。

  1. 優(yōu)點(diǎn):
    通常性能較高,因?yàn)椴恍枰M(jìn)行用戶態(tài)與內(nèi)核態(tài)的切換,避免了切換帶來的額外開銷,如上下文保存與恢復(fù)等。例如在無競爭的情況下,用戶態(tài)的自旋鎖和互斥鎖都可以快速地獲取和釋放鎖,執(zhí)行時間相對較短.

  2. 缺點(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)境下能夠正確地讀取和寫入這個字段的值

//例子1
    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;//5秒之后,work方法應(yīng)該結(jié)束循環(huán)
        }
        private static void Worker()
        {
            int x = 0;
            while (!s_stopWorker)
            {
                x++;
            }
            Console.WriteLine($"worker:stopped when x={x}");//在release模式下,該代碼不執(zhí)行。陷入了死循環(huán)出不來
        }
    }

JIT編譯優(yōu)化的時候,發(fā)現(xiàn)while (!s_stopWorker)中的s_stopWorker在該方法中永遠(yuǎn)不會變。因此就自作主張直接生成了while(ture)來“優(yōu)化”代碼

//例子2class 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)//一個死循環(huán),如果鎖競爭激烈就會占用CPU時間片
            {
                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);;//讓CPU偷個懶,不要這么努力的運(yùn)行
            }
        }
        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)鎖

  1. 優(yōu)點(diǎn):
    內(nèi)核態(tài)鎖由操作系統(tǒng)內(nèi)核管理和調(diào)度,當(dāng)鎖被釋放時,內(nèi)核可以及時地喚醒等待的線程,適用于復(fù)雜的同步場景和長時間等待的情況.

  2. 缺點(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)

  1. 自旋鎖(Thinlock):CoreCLR中別名瘦鎖

  2. 內(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一定要鎖一個引用類型吧?

眼見為實(shí):在自旋鎖下ObjectHeader存入了線程Id

點(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;//1. 線程首先讀取counter的當(dāng)前值,存為oldvalue
            newValue = new DualCounter(oldValue.A + 1, oldValue.B + 1);//2. 計(jì)算出新的值,作為預(yù)期值
        }
        while (Interlocked.CompareExchange(ref counter, newValue, oldValue) != oldValue);//3. 利用原子操作比較兩者的值,如果操作失敗,說明counter的值已經(jīng)被其它線程修改,需要重新讀取,直到成功。
        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)

上面提到的無鎖算法不一定比使用線程快。比如

  1. 每次都要New對象分配內(nèi)存,這個取決于你的業(yè)務(wù)復(fù)雜度。

  2. 如果Interlocked.CompareExchange一直交換失敗,會類似自旋鎖一樣大量占用CPU資源

簡單匯總一下

  1. 優(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ā)訪問。

  1. 缺點(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 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved