需要澄清有关 Thread.MemoryBarrier() 的信息

本文关键字:MemoryBarrier 信息 Thread | 更新日期: 2023-09-27 18:14:33

可能的重复项:
为什么我们需要 Thread.MemoryBarrier((?

简而言之,来自O'Reilly的C#:

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. 据我了解,屏障阻止在遵循指令后在其位置之前执行指令,我说得对吗?

需要澄清有关 Thread.MemoryBarrier() 的信息

内存屏障对从内存读取和写入强制实施排序约束:屏障发生之前的内存访问操作 - 屏障之后的内存访问操作。

    屏障 1
  1. 和障碍 4 具有互补的作用:屏障 1 确保写入_answer发生在写入_complete之前,而屏障 4 确保从_complete读取之前发生读取_answer想象一下障碍 4 不存在,但障碍 1 在那里。虽然保证在写入true之前将123写入_answer_complete其他正在运行的线程B()可能仍会重新排序其读取操作,因此它可能在读取_complete之前读取_answer。类似地,如果屏障 1 被移除并保留屏障 4:虽然从B()中的_complete读取总是会发生 - 在从_answer读取之前,_complete仍然可以在_answer之前被运行A()的其他线程写入。

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

  4. 内存屏障
  5. 只是确保内存访问操作的效果跨屏障排序。其他指令(例如递增寄存器中的值(仍可重新排序。

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

  1. 需要屏障 4 来使线程运行方法 B 读取 _answer 的最新值(即读取 123 而不是 0(。如果在发布模式下编译,编译器可能会优化代码并重新排序指令,以便运行 B 的线程可以读取 0,即使您编写的指令在逻辑上使这不可能(因为_answer是在 _complete 之前分配的(。
  2. 屏障 2 和 3 还阻止重新排序(以及缓存 _complete 的值(,因此运行 B 的线程不可能将_complete读取为 false,前提是它在 A 之后运行。
  3. 答案在上面。