需要澄清关于Thread.MemoryBarrier()的内容

4

可能是重复问题:
为什么我们需要Thread.MemoryBarrier()?

来自O'Reilly的C# in a Nutshell:

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

假设A和B方法在不同的线程上同时运行:
作者说:“屏障1和4防止此示例编写“0”。屏障2和3提供新鲜度保证:它们确保如果B在A之后运行,则读取_complete将评估为真。”
我的问题是:
  1. 为什么需要屏障4?屏障1不足以吗?
  2. 为什么需要2和3?
  3. 据我所了解,屏障防止在其位置之前执行指令,在其后续指令之后执行,我是正确的吗?

2
对你最有益的主要澄清是:永远不要编写需要任何这些内容的代码。使用“lock”、“InterLocked”和其他标准类。 - H H
这更像是 https://dev59.com/33A75IYBdhLWcg3wDUm2 的副本,而不是链接的那个。 - Martin Brown
2个回答

7

内存屏障强制对内存的读写进行排序:在屏障之前的内存访问操作发生在屏障之后的内存访问之前。

  1. 屏障1和4具有互补的作用:屏障1确保对_answer写入发生在对_complete的写入之前,而屏障4确保对_complete读取发生在对_answer的读取之前。想象一下如果没有屏障4,但是有屏障1。虽然可以保证在将true写入_complete之前将123写入_answer,但是运行B()的某些其他线程仍可能重新排序其读取操作,因此它可能在读取_complete之前读取_answer。同样,如果去掉屏障1而保留屏障4:虽然B()中从_complete的读取将始终发生在从_answer的读取之前,但是A()中的某些其他线程仍可能在将_answer写入之前将_complete写入。

  2. 屏障2和3提供新鲜度保证:如果在屏障2之后执行屏障3,则线程在执行屏障2时看到的状态对于在执行屏障3时运行B()的线程也是可见的。如果没有这两个屏障中的任何一个,那么在A()完成后执行的B()可能无法看到A()所做的更改。特别地,屏障2防止将写入_complete的值缓存到运行A()的处理器中,并强制处理器将其写出到主内存。同样,屏障3防止运行B()的处理器依赖缓存来获取_complete的值,而强制从主内存中读取。但请注意,在没有内存屏障2和3的情况下,过期的缓存不是阻止新鲜度保证的唯一因素。内存总线上操作的重新排序是另一个这种机制的例子。

  3. 内存屏障只确保内存访问操作的效果在屏障之间进行排序。其他指令(例如在寄存器中增加一个值)仍可能被重新排序。


我认为屏障2主要是必需的,以确保true实际上被写入_complete。如果没有它,编译器可能会在处理器寄存器中缓存该值一段时间。这特别重要,因为函数A和B非常简单,它们很可能被内联到调用方法中。同样,屏障3需要确保从内存中新读取_complete,并且不使用缓存的副本。 - Martin Brown
谢谢,但有两个小问题:首先,为了确保:如果CPU缓存在B中的执行路径知道之前就缓存了_answer(因为WriteLineif分支内部),那么需要屏障 4 吗?其次:我认为您可能会感到困惑 - 如果不是,请纠正我以确保不是我的困惑 - 关于:“……确保true被写入_complete之前123被写入_answer...”,我认为您的意思是相反的:“…确保123被写入_answer之前true被写入_complete…”。 - Tar
1
第一:没错。第二:确实。我已经纠正了错误。 - Adam Zalcman

1

好的,我们开始吧: 内存屏障可以防止优化编译器重新排序指令。这意味着在屏障之前的任何指令都不能在跟随屏障的指令之后执行。有几种类型的屏障,但我不会详细介绍。此外,具有弱内存排序的CPU可以重新排序指令并可能创建死锁。所以:

  1. 需要Barrier 4才能使运行方法B的线程读取最新的_answer值(即读取123而不是0)。如果您在发布模式下编译代码,则编译器可能会优化代码并重新排序指令,以便运行B的线程可能读取0,即使您编写的指令逻辑上使这种情况不可能发生(因为_answer在_complete之前被赋值)。
  2. Barrier 2和3也可以防止重新排序(以及缓存_complete的值),从而确保在A运行后,运行B的线程永远不会将_complete读取为false。
  3. 答案如上。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接