C# 中的那些锁,在内核态都是怎么保证同步的?
<h2 id="一背景">一:背景</h2><h3 id="1-讲故事">1. 讲故事</h3>
<p>其实这个问题是前段时间有位朋友咨询我的,由于问题说的比较泛,不便作答,但想想梳理一下还是能回答一些的,这篇就来聊一聊下面这几个锁。</p>
<ol>
<li>
<p>Interlocked</p>
</li>
<li>
<p>AutoResetEvent / ManualResetEvent</p>
</li>
<li>
<p>Semaphore</p>
</li>
</ol>
<p>用户态层面我就不想说了,网上一搜一大把,我们只聊一聊内核态。</p>
<h2 id="二锁玩法介绍">二:锁玩法介绍</h2>
<h3 id="1-interlocked">1. Interlocked</h3>
<p>从各种教科书上就可以知道,这个锁非常轻量级,也是各种高手善用的一把锁,为了方便说明,先上一段代码。</p>
<pre><code class="language-C#">
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();
}
}
</code></pre>
<p>这里我们在第二处 <code>Interlocked.Increment(ref location);</code> 下一个断点,目的是因为此时的 <code>Increment</code> 函数是 JIT 编译后的方法,接下来我们在 WinDbg 中单步调试,会看到如下汇编指令。</p>
<pre><code class="language-C#">
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 ,eax ds:00000000`001ceb68=00000002
</code></pre>
<p>看到上面的 <code>lock xadd</code> 了吗? 原来 <code>Interlocked</code> 类是借助了 CPU 提供的 锁机制 来解决线程同步的, 很显然这种级别的锁相比其他方式的锁性能伤害最小。</p>
<h3 id="2-autoreseteventmanualresetevent">2. AutoResetEvent,ManualResetEvent</h3>
<p>大家都知道这种锁的名字叫 <code>事件锁</code>, 其实在 Windows 上使用场景特别广,就连监视锁(Monitor) 底层也是用的这种<strong>事件锁</strong>, 不得不感叹其威力无穷! 而且代码注释中也说了,也就两种状态: <code>有信号</code> 和 <code>无信号</code> , 言外之意就是在内核中用了一个 <code>bool</code> 变量来表示,为了能看到这个 bool 值,我们上一个案例。</p>
<pre><code class="language-C#">
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();
}
}
</code></pre>
<p><img src="https://img2022.cnblogs.com/blog/214741/202209/214741-20220921130558249-55338654.png" alt="" loading="lazy"></p>
<p>为了找到 <code>handle=23c</code> 所对应的内核地址,可以借助 <code>Process Explorer</code> 工具,截图如下:</p>
<p><img src="https://img2022.cnblogs.com/blog/214741/202209/214741-20220921130558421-889007802.png" alt="" loading="lazy"></p>
<p>接下来启动 WinDbg 双机调试,看下内核态上 <code>ffffe00155522220</code> 内存位置的内容。</p>
<pre><code class="language-C#">
0: kd> dp 0xFFFFE00155522220 L1
ffffe001`5552222000000000`00060000
</code></pre>
<p>在控制台上将 <code>ManualResetEvent</code> 设为有信号模式,再次观察这块内存。</p>
<pre><code class="language-C#">
1: kd> dp 0xFFFFE00155522220 L1
ffffe001`5552222000000001`00060000
</code></pre>
<p>大家可以仔细试试看,会发现 <code>ffffe00155522220+0x4</code> 的位置一直都是 0,1 之间的切换,可以推测此时是一个 bool 类型。</p>
<p>有些朋友很好奇,能不能观察看到它的调用栈呢?肯定是可以的,我们使用 <code>ba</code> 下一个硬件断点,观察下它的用户态和内核态栈。</p>
<pre><code class="language-C#">
1: kd> ba w4 0xFFFFE00155522220+0x4
1: kd> g
Breakpoint 0 hit
nt!KeResetEvent+0x32:
fffff802`f8c3e752 f081237ffffffflock and dword ptr ,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
...
</code></pre>
<p>从代码中可以看到,命中的是 <code>KeResetEvent</code> 函数,也就是我们用户态代码的 <code>mre.Reset();</code> 函数,如果大家感兴趣,可以挖一下它的汇编代码,很清楚的看到这个方法中有一些 lock 语句,所以性能上会所有下降哈。</p>
<p><img src="https://img2022.cnblogs.com/blog/214741/202209/214741-20220921130558221-1394584055.png" alt="" loading="lazy"></p>
<h3 id="3-semaphore">3. Semaphore</h3>
<p>要说 Event 事件锁维护的是 bool 变量,那 Semaphore 就属于 int 变量了,为了方便说明继续上一个例子,观察方式和 Event 基本一致。</p>
<pre><code class="language-C#">
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();
}
}
</code></pre>
<p><img src="https://img2022.cnblogs.com/blog/214741/202209/214741-20220921130558100-1155117380.png" alt="" loading="lazy"></p>
<p>接下来用 WinDbg 进入到本机内核态观察 <code>handle=270</code> 所对应的 内核地址 <code>0xFFFFB58FEA1B1190</code>。</p>
<p><img src="https://img2022.cnblogs.com/blog/214741/202209/214741-20220921130558313-48363061.png" alt="" loading="lazy"></p>
<p>从图中可以非常清楚的看到这里的数字在不断的减小,其实想也能想到,少不了一些 CPU 级 lock 锁在里面。</p><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/16715237.html
頁:
[1]