C# 中的那些锁,在内核态都是怎么保证同步的?

打印 上一主题 下一主题

主题 872|帖子 872|积分 2616

一:背景

1. 讲故事

其实这个问题是前段时间有位朋友咨询我的,由于问题说的比较泛,不便作答,但想想梳理一下还是能回答一些的,这篇就来聊一聊下面这几个锁。

  • Interlocked
  • AutoResetEvent / ManualResetEvent
  • Semaphore
用户态层面我就不想说了,网上一搜一大把,我们只聊一聊内核态。
二:锁玩法介绍

1. Interlocked

从各种教科书上就可以知道,这个锁非常轻量级,也是各种高手善用的一把锁,为了方便说明,先上一段代码。
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             int location = 1;
  6.             Interlocked.Increment(ref location);
  7.             Console.WriteLine(location);
  8.             Debugger.Break();
  9.             Interlocked.Increment(ref location);
  10.             Console.WriteLine(location);
  11.             Console.ReadLine();
  12.         }
  13.     }
复制代码
这里我们在第二处 Interlocked.Increment(ref location); 下一个断点,目的是因为此时的 Increment 函数是 JIT 编译后的方法,接下来我们在 WinDbg 中单步调试,会看到如下汇编指令。
  1. 0:000> bp 00007ff8`f6d4298e
  2. 0:000> g
  3. Breakpoint 0 hit
  4. ConsoleApp2!ConsoleApp2.Program.Main+0x4e:
  5. 00007ff8`f6d4298e e84550ffff      call    00007ff8`f6d379d8
  6. 0:000> t
  7. 00007ff8`f6d379d8 e9439a7e5a      jmp     System_Private_CoreLib!System.Int32 System.Threading.Interlocked::Increment(System.Int32&)$##6002C3E (00007ff9`51521420)
  8. 0:000> t
  9. System_Private_CoreLib!System.Threading.Interlocked.Increment:
  10. 00007ff9`51521420 b801000000      mov     eax,1
  11. 0:000> t
  12. System_Private_CoreLib!System.Threading.Interlocked.Increment+0x5:
  13. 00007ff9`51521425 f00fc101        lock xadd dword ptr [rcx],eax ds:00000000`001ceb68=00000002
复制代码
看到上面的 lock xadd 了吗? 原来 Interlocked 类是借助了 CPU 提供的 锁机制 来解决线程同步的, 很显然这种级别的锁相比其他方式的锁性能伤害最小。
2. AutoResetEvent,ManualResetEvent

大家都知道这种锁的名字叫 事件锁, 其实在 Windows 上使用场景特别广,就连监视锁(Monitor) 底层也是用的这种事件锁, 不得不感叹其威力无穷! 而且代码注释中也说了,也就两种状态: 有信号 和 无信号 , 言外之意就是在内核中用了一个 bool 变量来表示,为了能看到这个 bool 值,我们上一个案例。
  1.     internal class Program
  2.     {
  3.         static ManualResetEvent mre = new ManualResetEvent(true);
  4.         static void Main(string[] args)
  5.         {
  6.             Console.WriteLine("handle=" + mre.Handle.ToString("x"));
  7.             for (int i = 0; i < 100; i++)
  8.             {
  9.                 mre.Reset();
  10.                 Console.WriteLine($"{i}:当前为阻塞模式,请观察");
  11.                 Console.ReadLine();
  12.                 mre.Set();
  13.                 Console.WriteLine($"{i}:当前为畅通模式,请观察");
  14.                 Console.ReadLine();
  15.             }
  16.             Console.ReadLine();
  17.         }
  18.     }
复制代码

为了找到 handle=23c 所对应的内核地址,可以借助 Process Explorer 工具,截图如下:

接下来启动 WinDbg 双机调试,看下内核态上 ffffe00155522220 内存位置的内容。
  1. 0: kd> dp 0xFFFFE00155522220 L1
  2. ffffe001`55522220  00000000`00060000
复制代码
在控制台上将 ManualResetEvent 设为有信号模式,再次观察这块内存。
  1. 1: kd> dp 0xFFFFE00155522220 L1
  2. ffffe001`55522220  00000001`00060000
复制代码
大家可以仔细试试看,会发现 ffffe00155522220+0x4 的位置一直都是 0,1 之间的切换,可以推测此时是一个 bool 类型。
有些朋友很好奇,能不能观察看到它的调用栈呢?肯定是可以的,我们使用 ba 下一个硬件断点,观察下它的用户态和内核态栈。
  1. 1: kd> ba w4 0xFFFFE00155522220+0x4
  2. 1: kd> g
  3. Breakpoint 0 hit
  4. nt!KeResetEvent+0x32:
  5. fffff802`f8c3e752 f081237fffffff  lock and dword ptr [rbx],0FFFFFF7Fh
  6. 0: kd> k
  7. # Child-SP          RetAddr               Call Site
  8. 00 ffffd000`ac0cea90 fffff802`f910ebd0     nt!KeResetEvent+0x32
  9. 01 ffffd000`ac0ceac0 fffff802`f8d59b63     nt!NtClearEvent+0x50
  10. 02 ffffd000`ac0ceb00 00007fff`d8963c0a     nt!KiSystemServiceCopyEnd+0x13
  11. 03 000000c9`10ece4d8 00007fff`d5e0057a     ntdll!NtClearEvent+0xa
  12. 04 000000c9`10ece4e0 00007fff`b88fba05     KERNELBASE!ResetEvent+0xa
  13. 05 000000c9`10ece510 00000000`00000000     System_Private_CoreLib!System.Boolean Interop+Kernel32::ResetEvent(Microsoft.Win32.SafeHandles.SafeWaitHandle)$##60000B0+0x65
  14. ...
复制代码
从代码中可以看到,命中的是 KeResetEvent 函数,也就是我们用户态代码的 mre.Reset(); 函数,如果大家感兴趣,可以挖一下它的汇编代码,很清楚的看到这个方法中有一些 lock 语句,所以性能上会所有下降哈。

3. Semaphore

要说 Event 事件锁维护的是 bool 变量,那 Semaphore 就属于 int 变量了,为了方便说明继续上一个例子,观察方式和 Event 基本一致。
  1.     internal class Program
  2.     {
  3.         static Semaphore semaphore = new Semaphore(10, 20);
  4.         static void Main(string[] args)
  5.         {
  6.             Console.WriteLine("handle=" + semaphore.Handle.ToString("x"));
  7.             for (int i = 0; i < 100; i++)
  8.             {
  9.                 semaphore.WaitOne();
  10.                 Console.WriteLine($"{i}:已减少 1,请观察");
  11.                 Console.ReadLine();
  12.             }
  13.             Console.ReadLine();
  14.         }
  15.     }
复制代码

接下来用 WinDbg 进入到本机内核态观察 handle=270 所对应的 内核地址 0xFFFFB58FEA1B1190。

从图中可以非常清楚的看到这里的数字在不断的减小,其实想也能想到,少不了一些 CPU 级 lock 锁在里面。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

农妇山泉一亩田

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表