其實這個問題是前段時間有位朋友諮詢我的,由於問題說的比較泛,不便作答,但想想梳理一下還是能回答一些的,這篇就來聊一聊下面這幾個鎖。
Interlocked
AutoResetEvent / ManualResetEvent
Semaphore
使用者態層面我就不想說了,網上一搜一大把,我們只聊一聊核心態。
從各種教科書上就可以知道,這個鎖非常輕量級,也是各種高手善用的一把鎖,為了方便說明,先上一段程式碼。
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 提供的 鎖機制 來解決執行緒同步的, 很顯然這種級別的鎖相比其他方式的鎖效能傷害最小。
大家都知道這種鎖的名字叫 事件鎖
, 其實在 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 語句,所以效能上會所有下降哈。
要說 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 鎖在裡面。