有人可以使用Threading.MemoryBarrier轻松解释如何在.Net中实现"Full Fences"吗?

Kil*_*fer 16 .net multithreading jit thread-safety

我清楚MemoryBarrier的用法,但不清楚运行时幕后发生的事情.任何人都可以对发生的事情做出很好的解释吗?

And*_*ass 16

在一个非常强大的内存模型中,不需要发出栅栏指令.所有内存访问都将按顺序执行,所有存储将全局可见.

需要内存防护,因为当前的常见体系结构不提供强大的内存模型 - 例如,x86/x64可以相对于写入重新排序读取.(更全面的资料来源是"英特尔®64和IA-32架构软件开发人员手册,8.2.2 P6和更近期处理器系列中的内存排序").作为gazillions的一个例子,Dekker的算法将在没有围栏的x86/x64上失败.

即使JIT生成机器代码,其中存储器加载和存储的指令被仔细放置,如果CPU然后重新排序这些负载和存储 - 它可以,只要保持当前的顺序一致性的错觉,它的努力是无用的.背景/线程.

冒险过度简化:它可能有助于将指令流产生的负载和存储可视化为一群雷鸣般的野生动物.当他们穿过一座狭窄的桥(你的CPU)时,你永远无法确定动物的顺序,因为它们中的一些会变慢,一些更快,一些超车,一些落后.如果在开始时 - 当您发出机器代码时 - 通过在它们之间放置无限长的栅栏将它们分成组,您至少可以确定组A在组B之前.

Fences确保读写顺序.措辞不准确,但是:

  • 商店围栏"等待"所有未完成的商店(写)操作完成,但不影响负载.
  • 加载围栏"等待"所有未完成的加载(读取)操作完成,但不影响商店.
  • 一个完整的栅栏"等待"所有存储和加载操作完成.它具有以下效果:在围栏之前的读取和写入将在"围栏的另一侧"(比围栏更晚)的写入和加载之前执行.

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 
Run Code Online (Sandbox Code Playgroud)

英特尔®64和IA-32架构软件开发人员手册第8.1.2节:

  • "...锁定操作序列化所有未完成的加载和存储操作(即等待它们完成)." ...... "锁定操作对于所有其他内存操作和所有外部可见事件都是原子操作.只有指令获取和页表访问才能传递锁定指令.锁定指令可用于同步由一个处理器写入的数据并由另一个处理器读取".

  • 内存排序指令满足了这一特定需求.MFENCE在上述情况下可能被用作完全屏障(至少在理论上 - 对于一个,锁定操作可能更快,对于两个,它可能导致不同的行为).MFENCE它的朋友可以在第8.2.5章"强化或弱化记忆排序模型"中找到.

还有一些方法可以序列化存储和加载,尽管它们比上述方法更不实用或更慢:

  • 在第二章8.3,你可以找到完整的序列化指令一样CPUID.这些序列化指令流也是:"没有什么可以传递序列化指令,并且序列化指令不能传递任何其他指令(读,写,取指令或I/O)".

  • 如果将内存设置为强无法缓存(UC),它将为您提供强大的内存模型:不允许进行推测性或无序访问,并且所有访问都将出现在总线上,因此无需发出指令.:)当然,这将比平常慢一点.

...

所以这取决于.如果有一台具有强大订购保证的计算机,JIT可能不会发出任何东西.

IA64和其他架构有自己的内存模型 - 从而保证内存排序(或缺少它们) - 以及它们自己的指令/方式来处理内存存储/加载顺序.