有人可以简单解释一下在.Net中如何使用Threading.MemoryBarrier实现“Full Fences”吗?
有人可以简单解释一下在.Net中如何使用Threading.MemoryBarrier实现“Full Fences”吗?
我对MemoryBarrier的使用有清楚的理解,但不清楚在运行时背后发生了什么。有没有人能够给出一个良好的解释,说明发生了什么?
在进行无锁并发编程时,程序指令重排是需要注意的。
程序指令重排可以发生在以下几个阶段:
- C#/VB.NET/F# 编译器优化
- JIT 编译器优化
- CPU 优化。
内存屏障是确保程序指令特定顺序的唯一方法。基本上,内存屏障是一类指令,它会导致 CPU 强制执行顺序约束。内存屏障可以分为三类:
- 加载屏障 - 确保无加载 CPU 指令穿过屏障
- 存储屏障 - 确保无存储 CPU 指令穿过屏障
- 完整屏障 - 确保无加载或存储 CPU 指令穿过屏障
在 .NET Framework 中,有很多方法来发射内存屏障:Interlock、Monitor、ReaderWriterLockSlim 等。
Thread.MemoryBarrier 在 JIT 编译器和处理器级别上都发射完整的内存屏障。
在一个真正强的内存模型中,发出屏障指令是不必要的。所有内存访问将按顺序执行,并且所有存储将是全局可见的。
由于当前常见的架构没有提供强内存模型,因此需要内存屏障-例如,x86 / x64可以重新排列相对于写入的读取。 (更全面的来源是“Intel 64 and IA-32 Architectures Software Developer’s Manual”,8.2.2 P6及更多处理器家族中的内存排序)。作为来自无数的示例,x86 / x64上的Dekker算法将在没有栅栏的情况下失败。
即使JIT生成带有内存读取和存储的指令的机器代码十分谨慎,如果CPU随后重新对这些加载和存储进行重新排序-只要保持当前上下文/线程的顺序一致的幻觉就可以,其努力就是无用的。
冒着过度简化的风险:可能有助于将源自指令流的加载和存储视为一群狂热的野兽。
当它们穿过一个狭窄的桥梁(你的CPU)时,你永远无法确定动物的顺序,因为它们中的一些人会更慢,有些人更快,有些人会超车,有些人会落后。如果在开始时-当您发出机器代码时-通过在它们之间放置无限长的栅栏来将它们分成组,您至少可以确保A组在B组之前。
栅栏确保读取和写入的顺序。措辞不是确切的,但:
- 存储栅栏“等待”所有未完成的存储(写入)操作,但不影响加载。
- 加载栅栏“等待”所有未完成的加载(读取)操作,但不影响存储。
- 完整的栅栏“等待”所有存储和加载操作完成。它的效果是,在栅栏之前读取和写入将在“栅栏另一侧”的写入和加载之前执行(晚于栅栏)。
JIT为完整栅栏发出的指令取决于(CPU)架构及其提供的内存排序保证。由于JIT完全知道它运行的架构,因此可以发出正确的指令。
在我的x64机器上,使用.NET 4.0 RC时,它恰好是 lock or
。
int a = 0; 00000000 sub rsp,28h Thread.MemoryBarrier(); 00000004 lock or dword ptr [rsp],0 Console.WriteLine(a); 00000009 mov ecx,1 0000000e call FFFFFFFFEFB45AB0 00000013 nop 00000014 add rsp,28h 00000018 ret
Intel 64 and IA-32 Architectures Software Developer’s Manual第8.1.2章:
-
“...锁定操作会序列化所有未完成的加载和存储操作(即等待它们完成)。'...'锁定操作与所有其他内存操作和所有外部可见事件都是原子的。仅指令提取和页面表访问可以通过锁定指令。锁定指令可用于同步一个处理器写入的数据和另一个处理器读取的数据。”
-
内存排序指令解决了这个特定的需要。
MFENCE
可以在上述情况下用作完全障碍(至少在理论上如此-首先,锁定操作可能更快,对于两个结果有所不同)。MFENCE
及其朋友可以在第8.2.5章“加强或削弱内存排序模型”中找到。
还有一些序列化存储和加载的方法,尽管它们要么不切实际,要么比上述方法更慢:
-
在第8.3章中,您可以找到完整的序列化指令,例如
CPUID
。这些序列化指令的流程也如下:"不能通过序列化指令,序列化指令也不能通过任何其他指令(读取、写入、指令获取或I/O)"。 -
如果您将内存设置为强制未缓存(UC),它将为您提供强制的内存模型: 不允许进行推测性或乱序访问,所有访问将出现在总线上,因此不需要发出指令。:)当然,这将比通常稍微慢一些。
...
所以这取决于。如果有一台具有强有序保证的计算机,JIT可能不会发出任何内容。
IA64和其他体系结构都有自己的内存模型-因此有内存存储/加载排序的保证(或者缺少它们)-以及处理内存存储/加载排序的自己的指令/方法。