我对以下代码示例有疑问(摘自: http://www.albahari.com/threading/part4.aspx#_NonBlockingSynch)

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。”

我了解使用内存屏障如何影响指令记录,但这是什么 「新鲜度保证」 提到的是什么?

文章后面还使用了下面的例子:

static void Main()
{
    bool complete = false; 
    var t = new Thread (() =>
    {
        bool toggle = false;
        while (!complete) 
        {
           toggle = !toggle;
           // adding a call to Thread.MemoryBarrier() here fixes the problem
        }

    });

    t.Start();
    Thread.Sleep (1000);
    complete = true;
    t.Join();  // Blocks indefinitely
}

这个例子后面有这样的解释:

“这个程序永远不会终止,因为完整的变量被缓存在 CPU 寄存器中。在 while 循环内插入对 Thread.MemoryBarrier 的调用(或锁定读取完成)可以修复错误。”

那么再次...这里发生了什么?

有帮助吗?

解决方案

在第一种情况下,屏障1确保_answer被写入之前_complete。不管代码是如何写的,或如何编译器或CLR指示CPU,该存储器总线的读/写队列可以重新排序的请求。该屏障基本上说“然后再继续刷新队列”。同样,道闸4确保_answer_complete了。否则,CPU2可以重新排序的东西,看到一个老_answer一个“新” _complete

障碍2和3是,在某种意义上,没用。请注意,解释包括“后”字:即“......如果B A,......后跑了”。它是什么意思了B到A后运行?如果B和A是相同的CPU上,那么肯定的是,B可以是后。但在这种情况下,相同的CPU意味着没有存储器屏障问题。

因此,考虑B和在不同的CPU A上运行。现在,很像爱因斯坦的相对论,在不同的位置比较时代的概念/ CPU的并没有真正意义。 思考它的另一种方式 - 您可以编写代码,它可以告诉B是否A之后跑了?如果是这样,那么你很可能使用的内存壁垒做到这一点。否则,你不能告诉,这是没有意义的问。这也是类似Heisenburg的原则 - 如果你能看到它,你已经修改了该实验

但离开物理不谈,让我们说,你可以打开你的机器罩,和的是_complete的实际存储位置是真实的(因为A已经运行)。现在运行B.无屏障3,CPU2可能仍未见_complete为真。即,不是 “新鲜”。

但你可能无法打开你的电脑,看看_complete。也传达你的发现给B CPU2。你唯一的沟通是自己在做什么的CPU。因此,如果他们不能确定没有障碍前/后,问:“发生什么事情,如果它运行后,没有障碍B”的是没有意义的的。

顺便说一句,我不知道你有什么在C#中可用,但什么是典型的做法,什么是真正需要的代码示例#1是在写一个释放的屏障,并在读一个单一的获取障碍:

void A()
{
   _answer = 123;
   WriteWithReleaseBarrier(_complete, true);  // "publish" values
}

void B()
{
   if (ReadWithAcquire(_complete))  // subscribe
   {  
      Console.WriteLine (_answer);
   }
}

“订阅”不经常被用来描述这种情况,但“发布”这个词是。我建议你读线程香草萨特的文章。

这使在障碍的究竟的正确的地方。

有关代码示例#2,这不是一个真正的存储器阻挡问题,这是一个编译器的优化问题 - 它是保持complete在寄存器中。内存屏障会迫使它,正如volatile,但可能会因此调用外部功能 - 如果编译器就无法知道外部函数修改complete与否,它会从内存中重新读取它。即也许通过complete的一些功能的地址(某处定义,其中编译器不能检查其详情):

while (!complete)
{
   some_external_function(&complete);
}

,即使该函数不修改complete,如果编译器是不知道,则需要重新加载其寄存器。

即代码1和代码2之间的区别是,代码1只有当A和B在单独的线程上运行的问题。代码2可以具有甚至单个螺纹机上的问题。

实际上,其他问题将是 - 可编译器完全除去while循环?如果它认为complete是其他代码无法访问的,为什么不呢?即,如果它决定complete移动到寄存器中,它也可能完全除去循环。

编辑:要回答来自OPC评论(我的答案是注释块太大):

屏障3个强制CPU冲洗任何挂起的读(写)请求。

所以,想象,如果有一些其他读取读取_complete之前:

void B {}
{
   int x = a * b + c * d; // read a,b,c,d
   Thread.MemoryBarrier();    // Barrier 3
   if (_complete)
   ...

如果没有屏障中,CPU可能具有所有这些5个的读请求'挂起':

a,b,c,d,_complete

如果没有屏障,处理器可以重新排列这些请求来优化存储器存取(即,如果_complete和“a”是在同一高速缓存行或东西)。

通过屏障中,CPU得到A,B,C,D从存储器背面BEFORE _complete甚至放在作为一个请求。确保“B”(例如)被读出之前_complete - 即没有重新排序

现在的问题是 - 这有什么区别

如果A,B,C,d是独立的从_complete,然后它并不重要。所有不通也缓慢下来。所以呀,_complete读的之后的。这样的数据是新鲜。把for循环睡眠(100)或一些忙等待在那里读之前,将使其“更新鲜”,以及! : - )

因此,问题是 - 保持它相对的。数据是否需要读/写的前/后的相对于其他一些数据或不?这是问题。

和不放下文章的作者 - 他确实提到“如果B A后跑了......”。它只是不完全清楚他是否被想象B之后A是到码是至关重要的,可观察到通过向代码,或者只是无关紧要的。

其他提示

<强>代码示例#1:

每个处理器核心包含的存储器的一部分的副本的高速缓存。这可能需要一些时间进行更新缓存。记忆障碍,保证高速缓存与主内存同步。举例来说,如果你没有障碍,2和3在这里,考虑这样的情况:

处理器1个运行A()。它_complete的新值写入其高速缓存(但不一定对主存储器还)。

处理器2个运行B()。它读取_complete的价值。如果此值在其高速缓存先前,它可能不是新鲜的(即不与主存储器同步),所以它不会得到更新后的值。

<强>代码样品#2:

一般情况下,变量被存储在存储器中。然而,假设一个值在单一函数多次读取:作为一种优化,编译器可以决定将其读入到一个CPU寄存器一次,然后访问在每次所需的时间寄存器。这是快得多,但防止从功能检测到的变化从另一个线程变量。

在存储器屏障这里迫使功能重新读取从存储器中的变量值。

调用Thread.MemoryBarrier()立即刷新,对于变量的实际值寄存器缓存。

在第一个例子中,将“新鲜度”为_complete是通过调用该方法设定它后右和右使用它之前提供。在第二个例子中,对于变量false初始complete值将在线程的自己的空间进行高速缓存,需要以立即看到从“内部”正在运行的线程实际的“外”的价值被重新同步。

“新鲜度”保证仅仅意味着障碍 2 和 3 强制执行以下值: _complete 尽快可见,而不是每当它们碰巧被写入内存时。

从一致性的角度来看,这实际上是不必要的,因为障碍 1 和 4 确保了 answer 读完后将继续阅读 complete.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top