2

我对以下代码示例有疑问(取自: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 将评估为真。”

我了解使用内存屏障如何影响指令重新排序,但是提到的“新鲜度保证”是什么?

在文章的后面,也用到了下面的例子:

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 的调用(或锁定读取完成)可以修复错误。”

再说一遍......这里会发生什么?

4

4 回答 4

6

在第一种情况下,屏障 1 确保_answer写入 BEFORE _complete。无论代码如何编写,或者编译器或 CLR 如何指示 CPU,内存总线读/写队列都可以重新排序请求。屏障基本上说“在继续之前刷新队列”。同样,屏障 4 确保_answer在 AFTER 之后读取_complete。否则 CPU2 可能会重新排序并看到旧_answer的带有 "new" _complete

从某种意义上说,障碍 2 和 3 是无用的。请注意,解释中包含单词“after”:即“...如果 B 在 A 之后运行,...”。B追着A是什么意思?如果 B 和 A 在同一个 CPU 上,那么当然,B 可以在之后。但在这种情况下,相同的 CPU 意味着没有内存屏障问题。

所以考虑 B 和 A 在不同的 CPU 上运行。现在,与爱因斯坦的相对论非常相似,比较不同位置/CPU 时间的概念并没有真正的意义。另一种思考方式 - 你能编写代码来判断 B 是否在 A 之后运行吗?如果是这样,那么您可能使用了内存屏障来做到这一点。否则,你说不出来,问也没有意义。它也类似于海森堡原理——如果你能观察到它,你就修改了实验。

但是抛开物理原理不谈,假设您可以打开机器的引擎盖,并查看的实际内存位置_complete是真实的(因为 A 已经运行)。现在运行 B. 没有障碍 3,CPU2 可能仍然不认为_complete是真的。即不“新鲜”。

但是您可能无法打开机器并查看_complete. 也不会将您的发现传达给 CPU2 上的 B。您唯一的交流是 CPU 本身在做什么。因此,如果他们无法在没有障碍的情况下确定之前/之后,那么询问“如果 B 在没有障碍的情况下运行在 A 之后会发生什么”是没有意义的

顺便说一句,我不确定您在 C# 中有什么可用的,但通常会做什么,以及代码示例 #1 真正需要的是写入时的单个释放屏障和读取时的单个获取屏障:

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

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

“订阅”这个词不常用于描述这种情况,但“发布”却是。我建议您阅读 Herb Sutter 关于线程的文章。

这将障碍放置在正确的位置。

对于代码示例 #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 在 _complete 甚至作为请求放入之前从内存中取回 a,b,c,d。ENSURING 'b'(例如)在 _complete 之前读取 - 即没有重新排序。

问题是 - 它有什么不同?

如果 a,b,c,d 独立于 _complete,那么没关系。障碍所做的只是让事情慢下来。所以,是的,稍后_complete阅读。所以数据比较新鲜。在读取之前在其中放置一个 sleep(100) 或一些忙等待循环也会使其“更新鲜”!:-)

所以重点是 - 保持相对。是否需要在相对于其他数据之前/之后读取/写入数据?这就是问题所在。

并且不要放下文章的作者——他确实提到了“如果 B 追赶 A……”。只是不清楚他是否认为 A 之后的 B 对代码至关重要,可以被代码观察到,或者只是无关紧要。

于 2009-11-15T06:12:21.607 回答
1

代码示例 #1:

每个处理器内核都包含一个缓存,其中包含一部分内存的副本。更新缓存可能需要一些时间。内存屏障保证缓存与主内存同步。例如,如果您在这里没有障碍 2 和 3,请考虑以下情况:

处理器 1 运行 A()。它将 _complete 的新值写入其缓存(但不一定写入主内存)。

处理器 2 运行 B()。它读取 _complete 的值。如果这个值以前在它的缓存中,它可能不是新鲜的(即,不与主内存同步),所以它不会得到更新的值。

代码示例 #2:

通常,变量存储在内存中。但是,假设在一个函数中多次读取一个值:作为一种优化,编译器可能决定将其读入 CPU 寄存器一次,然后在每次需要时访问该寄存器。这要快得多,但会阻止函数检测来自另一个线程的变量更改。

这里的内存屏障强制函数从内存中重新读取变量值。

于 2009-11-14T20:20:02.210 回答
0

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

在第一个例子中,“新鲜度”_complete是通过在设置之后和使用之前调用方法来提供的。在第二个示例中,false变量的初始值complete将缓存在线程自己的空间中,需要重新同步才能立即从正在运行的线程“内部”看到实际的“外部”值。

于 2009-11-14T20:15:29.457 回答
0

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

从一致性的角度来看,这实际上是不必要的,因为障碍 1 和 4 确保answer在阅读之后会被阅读complete

于 2009-11-16T00:09:04.610 回答