gpt4 book ai didi

c# - 内存屏障如何影响数据的 “freshness”?

转载 作者:搜寻专家 更新时间:2023-10-31 00:05:42 26 4
gpt4 key购买 nike

我对以下代码示例有疑问(取自: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);
}
}
}

接下来是以下移植:

"Barriers 1 and 4 prevent this example from writing “0”. Barriers 2 and 3 provide a freshness guarantee: they ensure that if B ran after A, reading _complete would evaluate to 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
}

此示例后有以下说明:

"This program never terminates because the complete variable is cached in a CPU register. Inserting a call to Thread.MemoryBarrier inside the while-loop (or locking around reading complete) fixes the error."



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

最佳答案

在第一种情况下,屏障1确保_answer之前写入_complete。无论代码是如何编写的,还是编译器或CLR如何指示CPU,内存总线的读/写队列都可以对请求进行重新排序。屏障基本上说“先清除队列再继续”。同样,屏障4确保在_answer之后读取_complete。否则,CPU2可能会重新排序,并看到带有"new" _answer的旧_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为true。即不是“新鲜”。

但是您可能无法打开计算机并查看_complete。也不要将您的发现传达给CPU2上的B。您唯一的通信就是CPU本身在做什么。因此,如果他们不能无障碍地确定事前/事后,那么问“如果B在没有障碍的情况下在A之后运行,B会发生什么”是没有道理的。

顺便说一下,我不确定您在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的区别在于,仅当A和B在单独的线程上运行时,代码1才有问题。即使在单线程计算机上,代码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,然后再将_complete作为请求放入。例如,在_complete之前读取确保为'b'的内容,即不进行重新排序。

问题是-它有什么区别?

如果a,b,c,d与_complete独立,那么没关系。所有的障碍都是放慢速度。是的, _complete稍后再读。因此,数据更加新鲜。在读取之前将sleep(100)或一些忙于等待的for-loop放入其中也会使其变得“新鲜”! :-)

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

而且不要压倒文章的作者-他确实提到“如果B追赶A ...”。尚不清楚他是否想像A之后的B对代码至关重要,可以通过代码观察还是无关紧要。

关于c# - 内存屏障如何影响数据的 “freshness”?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1735403/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com