内存栅栏如何影响数据的“新鲜度”?
-
20-09-2019 - |
题
我对以下代码示例有疑问(摘自: 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
.