在哪里放置栅栏/内存屏障以保证新的读取/提交写入?

时间:2014-02-08 23:10:19

标签: c# .net multithreading volatile memory-fences

像许多其他人一样,我一直对易失性读/写和围栏感到困惑。所以现在我想完全理解这些是做什么的。

因此,易失性读取应该(1)表现出获取语义,(2)保证读取的值是新鲜的,即它不是缓存值。让我们关注(2)。

现在,I've read,如果你想执行易失性读取,你应该在读取后引入一个获取栅栏(或一个完整的栅栏),如下所示:

int local = shared;
Thread.MemoryBarrier();

这究竟是如何阻止读取操作使用以前缓存的值? 根据围栏的定义(不允许读取/存储在围栏上方/下方移动),我会在读取之前插入围栏,防止读取越过围栏并被移动向后及时(又名,被缓存)。

如何防止读取及时向前移动(或后续指令不及时向后移动)可以保证读取不稳定(新鲜)?它有什么用?


类似地,我认为易失性写入应该在写入操作之后引入一个fence ,从而阻止处理器及时向前移动写入(也就是说,延迟写入)。我相信这会使处理器刷新对主存储器的写入。

但令我惊讶的是,C# implementation在写之前引入了围栏

[MethodImplAttribute(MethodImplOptions.NoInlining)] // disable optimizations
public static void VolatileWrite(ref int address, int value)
{
    MemoryBarrier(); // Call MemoryBarrier to ensure the proper semantic in a portable way.
    address = value;
}

更新

根据this example显然取自“Nutshell中的C#4”,围栏2,在写入之后放置应该强制写入立即刷新到主内存,并且在读取之前放置的围栏3应该保证新的读取:

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);
    }
  }
}

本书中的想法(以及我自己的个人信仰)似乎与C#的VolatileReadVolatileWrite实现背后的想法相矛盾。

2 个答案:

答案 0 :(得分:9)

  

这究竟是如何阻止读取操作使用a   以前缓存过的值?

没有这样的事情。易失性读取不保证将返回最新值。简单地说,所有它的意思是下一次读取将返回一个更新的值,而不是更多。

  

如何防止读取及时向前移动(或   随后的指示被及时向后移动保证   一个不稳定(新鲜)的读物?它有什么用?

请注意这里的术语。挥发性不是新鲜的同义词。正如我上面已经提到的,它的真正用处在于如何将两个或多个易失性读取链接在一起。一系列易失性读取中的下一次读取将绝对返回较新的值,而不是先前读取的相同地址。应该在考虑此前提的情况下编写无锁代码。也就是说,代码的结构应该适用于处理较新值而不是最新值的原则。这就是为什么大多数无锁代码在循环中旋转,直到它可以完全验证操作成功。

  

本书中的想法(以及我自己的个人信仰)似乎如此   与C#的VolatileRead和VolatileWrite背后的想法相矛盾   的实施方式。

不是真的。记得不稳定!=新鲜。是的,如果你想要一个新鲜的"然后读取你需要在读取之前放置一个获取栅栏。但是,这与进行易失性读取不同。我所说的是,如果VolatileRead的实现在读取指令之前调用了Thread.MemoryBarrier ,那么它实际上不会产生 volatile 读。如果会产生 fresh 读取。

答案 1 :(得分:6)

要理解的重要一点是volatile不仅意味着“不能缓存值”,而且还提供了重要的可见性保证(确切地说,完全有可能只有缓存的易失性写入;仅取决于硬件及其使用的缓存一致性协议)

易失性读取提供获取语义,而易失性写入具有释放语义。获取围栏意味着您无法在围栏之前重新排序读取或写入,而释放围栏意味着您无法在围栏之后移动它们linked answer in the comments解释说实际上相当不错。

现在的问题是,如果我们在加载之前没有任何内存障碍,我们将如何确保看到最新值?答案是:因为我们在每次volatile写操作之后也会设置内存屏障来保证这一点。

Doug Lea写了一篇很好的总结,介绍了哪些障碍存在,他们做了什么以及将它们放在哪里用于JMM的易失性读/写作为编译器编写者的帮助,但该文本对其他人也非常有用。易失性读写在Java和CLR中都提供了相同的保证,因此通常适用。

Source - 向下滚动到“Memory Barriers”部分(我会复制有趣的部分,但格式化不存在......)