【问题标题】:Volatile Reads / Writes and Thread.MemoryBarrier Ordering易失性读/写和 Thread.MemoryBarrier 排序
【发布时间】:2017-08-02 08:17:22
【问题描述】:

我正试图围绕内存屏障和易失性读/写的微妙之处展开思考。我在这里阅读 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 将评估为 true。

问题 #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(如您在上面看到的那样),则需要将值传递给/从方法传递,而不是作为局部变量内联,因此必须从内存/缓存访问而不是直接通过寄存器,从而消除了在写入之后和读取之前额外的内存屏障的需要。我不知道是不是这样,但这是我看到它正常工作的唯一方法。

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

【问题讨论】:

  • 微软把这件事搞砸了。他们使用 Volatile 类修复它,read the comment。
  • @HansPassant 据我所知,Volatile 方法速度更快,排序保证更少,但 Thread 方法仍然保证新读取等。经过更多挖掘后,我用我的理论更新了我的答案 - 对此有何其他评论?我试图了解易失性读取在读取后如何与内存屏障一起工作。

标签: c# thread-safety memory-barriers


【解决方案1】:

我认为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.VolatileRead 和Thread.VolatileWrite。

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

虽然 Volatile.Read 和 Volatile.Write 方法在 C# 中实现,就像 Thread.VolatileRead 和 Thread.VolatileWrite 一样,但 CLR 将方法替换为具有实际易失读/写语义的本机版本。

【讨论】:

  • 我知道内存屏障会阻止两者被移动,但由于 Volatile 方法的编写方式,如果您执行 Write 后跟 Read,处理器可能仍会重新排序它们是因为两者之间没有内存屏障,因此它不一定会阻止读取在写入之前移动。
  • Thread.VolatileRead 的文档声明它获得了最新的值。你是说文档不正确?在循环中它可能无关紧要,但它可能会对一次性检查是否是这种情况的代码产生很大影响。
  • “处理器可能仍然会重新排序它们,因为两者之间没有内存屏障” -- 是的,但任何可能的结果都是允许的。如果读取在同一个变量上,由于数据依赖性,它将具有正确的值;如果它位于不同的变量上,那么(来自 CLR 规范)“从所有执行线程来看,不需要一个符合要求的实现来提供 volatile 写入的单一总排序。” 因此,多个 volatile来自多个线程的写入可能以任何顺序发生。
  • “你是说文档不正确吗?” -- 是的(而且我不是唯一一个,因为它的价值),或者至少它取决于freshness 的定义,在读取指令时不一定是 current 或 latest。 “它可能会对执行一次性检查的代码产生很大影响” -- 如果您不同步(或以其他方式检测到易失性更改),则无论如何都没有正确的结果。您想到的哪种正确案例取决于这样一次检查?
  • 我知道这是允许的。您引用了“在读取之后和写入之前放置屏障确保不会在任何读取之前移动写入(但反之亦然)”并回答了有关内存屏障不允许重新排序两个内存访问的内容。我们在任何事情上都没有分歧,我只是向您展示我的陈述是如何正确的,因为您似乎误解了我想说的话并试图以某种方式纠正我。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-04-04
  • 2023-03-21
  • 2018-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多