C# 中的那些鎖,在核心態都是怎麼保證同步的?

2022-09-21 15:00:39

一:背景

1. 講故事

其實這個問題是前段時間有位朋友諮詢我的,由於問題說的比較泛,不便作答,但想想梳理一下還是能回答一些的,這篇就來聊一聊下面這幾個鎖。

  1. Interlocked

  2. AutoResetEvent / ManualResetEvent

  3. Semaphore

使用者態層面我就不想說了,網上一搜一大把,我們只聊一聊核心態。

二:鎖玩法介紹

1. Interlocked

從各種教科書上就可以知道,這個鎖非常輕量級,也是各種高手善用的一把鎖,為了方便說明,先上一段程式碼。


    internal class Program
    {
        static void Main(string[] args)
        {
            int location = 1;

            Interlocked.Increment(ref location);
            Console.WriteLine(location);

            Debugger.Break();

            Interlocked.Increment(ref location);
            Console.WriteLine(location);
            Console.ReadLine();
        }
    }

這裡我們在第二處 Interlocked.Increment(ref location); 下一個斷點,目的是因為此時的 Increment 函數是 JIT 編譯後的方法,接下來我們在 WinDbg 中單步偵錯,會看到如下組合指令。


0:000> bp 00007ff8`f6d4298e
0:000> g
Breakpoint 0 hit
ConsoleApp2!ConsoleApp2.Program.Main+0x4e:
00007ff8`f6d4298e e84550ffff      call    00007ff8`f6d379d8
0:000> t
00007ff8`f6d379d8 e9439a7e5a      jmp     System_Private_CoreLib!System.Int32 System.Threading.Interlocked::Increment(System.Int32&)$##6002C3E (00007ff9`51521420)
0:000> t
System_Private_CoreLib!System.Threading.Interlocked.Increment:
00007ff9`51521420 b801000000      mov     eax,1
0:000> t
System_Private_CoreLib!System.Threading.Interlocked.Increment+0x5:
00007ff9`51521425 f00fc101        lock xadd dword ptr [rcx],eax ds:00000000`001ceb68=00000002

看到上面的 lock xadd 了嗎? 原來 Interlocked 類是藉助了 CPU 提供的 鎖機制 來解決執行緒同步的, 很顯然這種級別的鎖相比其他方式的鎖效能傷害最小。

2. AutoResetEvent,ManualResetEvent

大家都知道這種鎖的名字叫 事件鎖, 其實在 Windows 上使用場景特別廣,就連監視鎖(Monitor) 底層也是用的這種事件鎖, 不得不感嘆其威力無窮! 而且程式碼註釋中也說了,也就兩種狀態: 有訊號無訊號 , 言外之意就是在核心中用了一個 bool 變數來表示,為了能看到這個 bool 值,我們上一個案例。


    internal class Program
    {
        static ManualResetEvent mre = new ManualResetEvent(true);

        static void Main(string[] args)
        {
            Console.WriteLine("handle=" + mre.Handle.ToString("x"));

            for (int i = 0; i < 100; i++)
            {
                mre.Reset();
                Console.WriteLine($"{i}:當前為阻塞模式,請觀察");
                Console.ReadLine();

                mre.Set();
                Console.WriteLine($"{i}:當前為暢通模式,請觀察");
                Console.ReadLine();
            }

            Console.ReadLine();
        }
    }

為了找到 handle=23c 所對應的核心地址,可以藉助 Process Explorer 工具,截圖如下:

接下來啟動 WinDbg 雙機偵錯,看下核心態上 ffffe00155522220 記憶體位置的內容。


0: kd> dp 0xFFFFE00155522220 L1
ffffe001`55522220  00000000`00060000

在控制檯上將 ManualResetEvent 設為有訊號模式,再次觀察這塊記憶體。


1: kd> dp 0xFFFFE00155522220 L1
ffffe001`55522220  00000001`00060000

大家可以仔細試試看,會發現 ffffe00155522220+0x4 的位置一直都是 0,1 之間的切換,可以推測此時是一個 bool 型別。

有些朋友很好奇,能不能觀察看到它的呼叫棧呢?肯定是可以的,我們使用 ba 下一個硬體斷點,觀察下它的使用者態和核心態棧。


1: kd> ba w4 0xFFFFE00155522220+0x4
1: kd> g
Breakpoint 0 hit
nt!KeResetEvent+0x32:
fffff802`f8c3e752 f081237fffffff  lock and dword ptr [rbx],0FFFFFF7Fh
0: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffd000`ac0cea90 fffff802`f910ebd0     nt!KeResetEvent+0x32
01 ffffd000`ac0ceac0 fffff802`f8d59b63     nt!NtClearEvent+0x50
02 ffffd000`ac0ceb00 00007fff`d8963c0a     nt!KiSystemServiceCopyEnd+0x13
03 000000c9`10ece4d8 00007fff`d5e0057a     ntdll!NtClearEvent+0xa
04 000000c9`10ece4e0 00007fff`b88fba05     KERNELBASE!ResetEvent+0xa
05 000000c9`10ece510 00000000`00000000     System_Private_CoreLib!System.Boolean Interop+Kernel32::ResetEvent(Microsoft.Win32.SafeHandles.SafeWaitHandle)$##60000B0+0x65
...

從程式碼中可以看到,命中的是 KeResetEvent 函數,也就是我們使用者態程式碼的 mre.Reset(); 函數,如果大家感興趣,可以挖一下它的組合程式碼,很清楚的看到這個方法中有一些 lock 語句,所以效能上會所有下降哈。

3. Semaphore

要說 Event 事件鎖維護的是 bool 變數,那 Semaphore 就屬於 int 變數了,為了方便說明繼續上一個例子,觀察方式和 Event 基本一致。


    internal class Program
    {
        static Semaphore semaphore = new Semaphore(10, 20);

        static void Main(string[] args)
        {
            Console.WriteLine("handle=" + semaphore.Handle.ToString("x"));

            for (int i = 0; i < 100; i++)
            {
                semaphore.WaitOne();
                Console.WriteLine($"{i}:已減少 1,請觀察");
                Console.ReadLine();
            }

            Console.ReadLine();
        }
    }

接下來用 WinDbg 進入到本機核心態觀察 handle=270 所對應的 核心地址 0xFFFFB58FEA1B1190

從圖中可以非常清楚的看到這裡的數位在不斷的減小,其實想也能想到,少不了一些 CPU 級 lock 鎖在裡面。