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

dca*_*tro 13 .net c# multithreading volatile memory-fences

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

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

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

int local = shared;
Thread.MemoryBarrier();
Run Code Online (Sandbox Code Playgroud)

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

如何防止读取被及时向前移动(或后续指令被及时向后移动)保证了易失性(新鲜)读取?它有什么用?


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

但令我惊讶的是,C#实现在写入之前引入了栅栏!

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

更新

根据这个例子,显然是从"坚果壳中的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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

Bri*_*eon 9

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

它没有这样的事情.易失性读取不保证将返回最新值.简单地说,所有它真正意味着下一次读取将返回一个更新的值,仅此而已.

如何防止读取被及时向前移动(或后续指令被及时向后移动)保证了易失性(新鲜)读取?它有什么用?

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

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

并不是的.记得不稳定!=新鲜.是的,如果您想要"新鲜"阅读,那么您需要在阅读之前放置一个获取栅栏.但是,这与进行易失性读取不同.我所说的是,如果执行在读取指令之前VolatileRead有调用那么它实际上不会产生易失性读取.如果会产生新鲜的读数.Thread.MemoryBarrier

  • 答案很好但不太正确.简而言之,遗憾的是,没有办法保证在运行多个内核的机器上进行"全新"读取.内存障碍只是保证了订购.它们无法保证在一个内核上运行的线程将读取同时写在不同内核上的值.如果这是可行的,那么就可以实现一个同步布尔来控制方法的多线程入口,并完全不需要锁. (4认同)

Voo*_*Voo 6

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

易失性读取提供获取语义,而易失性写入具有释放语义.获取围栏意味着您不能在围栏之前重新排序读取或写入,而释放围栏意味着您无法在围栏之后移动它们.评论中链接答案解释说实际上相当不错.

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

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

来源 - 向下滚动到"记忆障碍"部分(我会复制有趣的部分,但格式化不存在......)


归档时间:

查看次数:

1156 次

最近记录:

11 年,4 月 前