为什么我需要内存屏障?

hac*_*sid 34 c# multithreading thread-safety shared-memory memory-barriers

Nutshell中的C#4(强烈推荐的btw)使用以下代码来演示MemoryBarrier的概念(假设A和B在不同的线程上运行):

class Foo{
  int _answer;
  bool complete;
  void A(){
    _answer = 123;
    Thread.MemoryBarrier(); // Barrier 1
    _complete = true;
    Thread.MemoryBarrier(); // Barrier 2
  }
  void B(){
    Thread.MemoryBarrier(); // Barrier 3;
    if(_complete){
      Thread.MemoryBarrier(); // Barrier 4;
      Console.WriteLine(_answer);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

他们提到障碍1和4阻止这个例子写0和障碍2和3提供新鲜度保证:他们确保如果B在A之后运行,读_complete将评估为.

我不是真的得到它.我想我明白为什么壁垒1和4是必要的:我们不想在写_answer进行优化,并放置在写后_complete(屏障1),我们需要确保_answer没有被缓存(光栅4) .我也认为我理解为什么Barrier 3是必要的:如果A在写完_complete = true之后才运行,B仍然需要刷新_complete以读取正确的值.

我不明白为什么我们需要障碍2!我的一部分说这是因为可能线程2(运行B)已经运行直到(但不包括)if(_complete),因此我们需要确保_complete被刷新.

但是,我不知道这有多大帮助.是不是仍然可以在A 中将_complete设置为true但是B方法会看到_complete的缓存(错误)版本?即,如果线程2运行方法B直到第一个MemoryBarrier之后,然后线程1运行方法A直到_complete = true但没有进一步,然后线程1恢复并测试是否(_complete) - 如果不导致错误

Bri*_*eon 27

屏障#2 guarentees写入_complete立即承诺.否则它可能保持在排队状态,这意味着即使有效地使用易失性读取,读_completeB也不会看到由此引起的变化.AB

当然,这个例子并不能完全解决这个问题,因为A在写入之后不再做任何事情,_complete这意味着无论如何都会立即进行写入,因为线程会提前终止.

关于您是否if仍然可以评估的问题的答案false是肯定的,因为您说明的原因.但是,请注意作者关于这一点的说法.

障碍1和4阻止该示例写入"0".障碍2和3提供了新鲜度保证:它们确保如果B在A之后运行,则读取_complete将评估为真.

强调"如果B跑在A之后"是我的.当然可能是两个线程交错的情况.但是,作者忽略了这种情况,大概是为了说明如何Thread.MemoryBarrier更简单地工作.

顺便说一句,我很难在我的机器上设计一个例子,其中障碍#1和#2会改变程序的行为.这是因为关于写入的内存模型在我的环境中很强.也许,如果我有一台多处理器机器,使用Mono,或者有一些其他不同的设置我可以证明它.当然,很容易证明去除障碍#3和#4会产生影响.

  • @ohadsc:内存障碍只会影响单个线程的行为.考虑A和B可能在不同的CPU上运行.如果删除了障碍2,则可能不会提交写入.如果您删除了障碍3,则可能无法刷新读取.A中的障碍对B的执行没有影响,反之亦然. (5认同)
  • 我不明白内存屏障#4(有必要吗?)。#3 已经确保我们“无效”内存缓存并具有最新值。并且 _answer 保证首先有价值。我错过了什么? (2认同)

Iam*_*mIC 5

该示例不清楚有两个原因:

  1. 它太简单了,无法完全显示栅栏发生的情况。
  2. Albahari 包括对非 x86 架构的要求。请参阅MSDN:“仅在内存排序较弱的多处理器系统上才需要 MemoryBarrier(例如,采用多个 Intel Itanium 处理器的系统 [Microsoft 不再支持])。”。

如果您考虑以下几点,就会变得更清楚:

  1. 内存屏障(此处为全屏障 - .Net 不提供半屏障)可防止读/写指令跳过栅栏(由于各种优化)。这保证了栅栏之后的代码将在栅栏之前的代码之后执行。
  2. “这种序列化操作保证在 MFENCE 指令之后的任何加载或存储指令全局可见之前,按程序顺序位于 MFENCE 指令之前的每个加载和存储指令都是全局可见的。” 看这里
  3. x86 CPU 具有强大的内存模型,并保证写入对所有线程/核心都一致(因此 x86 上不需要屏障 #2 和 #3)。但是,我们不能保证读取和写入将保持编码顺序,因此需要屏障 #1 和 #4。
  4. 内存屏障效率低下,不需要使用(请参阅同一篇 MSDN 文章)。我个人使用 Interlocked 和 volatile(确保你知道如何正确使用它!!),它们工作高效且易于理解。

诗。本文很好地解释了 x86 的内部工作原理。