易失性读/写和Thread.MemoryBarrier排序

时间:2017-03-12 06:46:57

标签: c# thread-safety memory-barriers

我正试图围绕内存障碍和易失性读/写的细微差别。我在这里阅读Joseph Albahari的线程文章:

http://www.albahari.com/threading/part4.aspx

在读/写之前需要内存屏障以及何时需要内存屏障的问题上磕磕绊绊。在“完全围栏”部分的这段代码中,他在每次写入之后和每次阅读之前都设置了一个内存屏障:

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

他继续解释:

  

障碍1和4阻止此示例写入“0”。障碍2和   3提供新鲜度保证:他们确保如果B在A之后跑,   阅读_complete会评估为真。

问题#1:我对障碍1和障碍4没有任何问题,因为它会阻止跨越障碍的重新排序。我不完全理解为什么障碍2和障碍3是必要的。有人可以解释一下,特别是考虑到Thread类中如何实现易失性读写操作(下文解释)?

现在我真正开始感到困惑的是,这是Thread.VolatileRead/Write()的实际实现:

[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void VolatileWrite (ref int address, int value)
{
  MemoryBarrier(); address = value;
}

[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static int VolatileRead (ref int address)
{
  int num = address; MemoryBarrier(); return num;
}

正如您所看到的,与前一个示例相反,内置的volatile函数在每次写入之前(而不是之后)和每次读取之后(而不是之前)放置内存屏障。因此,如果我们使用基于内置volatile函数的等效版本重写前一个示例,它将看起来像这样:

class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    Thread.MemoryBarrier();    // Barrier 1
    _answer = 123;
    Thread.MemoryBarrier();    // Barrier 2
    _complete = true;
  }

  void B()
  {
    if (_complete)
    {
      Thread.MemoryBarrier();    // Barrier 3
      Console.WriteLine (_answer);
      Thread.MemoryBarrier();       // Barrier 4
    }
  }
}

问题#2:两个Foo类在功能上是否相同?为什么或者为什么不?如果需要障碍2和3(在第一个Foo类中)来保证写入值并读取实际值,那么Thread.VolatileXXX方法是否会变得无用?

在StackOverflow上有几个类似的问题,接受的答案如“屏障2确保写入_complete没有被缓存”,但它们都没有解决为什么Thread.VolatileWrite()在写入之前放置内存屏障的原因,如果这是如果Thread.VolatileRead()在读取后放置内存屏障但仍保证最新值,则需要屏障3的情况。我认为这就是最让我失望的原因。

更新

好的,所以经过更多的阅读和思考后,我有一个理论,我用源代码更新了我认为可能相关的属性。我不认为Thread.VolatileRead/Write方法中的内存障碍可以确保值的“新鲜度”,而是强制执行重新排序保证。在读取之后和写入之前设置屏障可确保在任何读取之前不会移动写入(但反之亦然)。

从我能够找到的,x86上的所有写入都通过使其他内核上的缓存行无效来保证缓存一致性,因此只要该值未缓存在寄存器中,就可以保证“新鲜度”。我在VolatileRead/Write确保价值的理论不在寄存器中,这可能是关闭但我认为我在正确的轨道上,是他们依靠.NET实现细节,如果它们被标记为MethodImplOptions.NoInlining(正如您在上面看到的那样),则需要将值传递给方法/从方法传递,而不是作为局部变量内联,因此必须访问从内存/缓存而不是直接通过寄存器,因此在写入之后和读取之前不需要额外的内存屏障。我不知道是不是这样,但这是我能看到它正常工作的唯一方法。

任何人都可以确认或否认这种情况吗?

1 个答案:

答案 0 :(得分:0)

  

我认为Thread.VolatileRead/Write方法中的内存障碍根本不能确保值的“新鲜度”,而是强制执行重新排序保证。

没错。

  

在读取之后和写入之前设置屏障可确保在任何读取之前不会移动写入(但反之亦然)。

完整的内存屏障同时具有获取和释放语义,它可以防止先前的内存访问被重新排序到屏障之后以及后续的内存访问被重新排序到屏障之前。

  

任何人都可以确认或否认这种情况吗?

对于x86中的Microsoft .NET实现中的写入,您可能是正确的,但这同样适用于读取。读取可以在之前的访问和内存屏障之间重新排序,可以是JIT编译器(可能不是no-inlining属性),也可以是CPU(即使使用no-inlining属性)。

但是,这不应该改变正在运行的代码看到的内容,尽管读取可能看不到 freshest 值。

int value = 0;
bool done = false;

// in thread 1
value = 123;
Thread.VolatileWrite(ref done, true);

// in thread 2
Thread.SpinUntil(() => Thread.VolatileRead(ref done));
Console.WriteLine(value); // guaranteed 123 due to the memory barrier

在具有较弱内存模型的其他体系结构中,写入可以在后续内存访问之后重新排序,并且在极端情况下,在下一个内存屏障之前,它可能不会对其他线程可见。但是使用循环,这不是什么大问题。

无论如何,我的建议是不要使用Thread.VolatileReadThread.VolatileWrite

读取和写入volatile字段,Volatile.ReadVolatile.Write方法提供正确的语义。

尽管Volatile.ReadVolatile.Write方法在C#中实现,就像Thread.VolatileReadThread.VolatileWrite一样,但CLR用具有实际volatile读/写的本机版本替换方法语义。