2 c# c++ multithreading memory-model lock-free
我对以下代码示例有疑问(摘自: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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
接下来是以下解释:
"障碍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
}
Run Code Online (Sandbox Code Playgroud)
这个例子后面是这样的解释:
"这个程序永远不会终止,因为完整的变量被缓存在CPU寄存器中.在while循环中插入对Thread.MemoryBarrier的调用(或者在读取完成时锁定)可以修复错误."
再说一次......这里发生了什么?
在第一种情况下,Barrier 1确保_answer在之前编写_complete.无论代码是如何编写的,或者编译器或CLR如何指示CPU,内存总线读/写队列都可以对请求进行重新排序.障碍基本上说"在继续之前冲洗队列".同样,Barrier 4确保_answer在之后阅读_complete.否则CPU2可以重新排序,看到旧_answer的"新" _complete.
在某种意义上,障碍2和3是无用的.请注意,解释包含单词"after":即"......如果B在A之后运行,......".B在A之后跑的意味着什么?如果B和A在同一个CPU上,那么肯定,B可以在之后.但在这种情况下,相同的CPU意味着没有内存屏障问题.
因此,考虑在不同的CPU上运行B和A. 现在,非常像爱因斯坦的相对论,在不同位置/ CPU上比较时间的概念并不真正有意义.另一种思考方式 - 你能编写代码来判断B是否在A之后运行?如果是这样,你可能会使用记忆障碍来做到这一点.否则,你无法分辨,并且没有任何意义.它也类似于海森堡的原则 - 如果你能观察它,你就修改了实验.
但是把物理放在一边,让我们说你可以打开机器的引擎盖,看看实际的内存位置_complete是真的(因为A已经运行).现在运行B.没有Barrier 3,CPU2可能仍然看不到_complete真实.即不"新鲜".
但是你可能无法打开机器看看_complete.也不会将您的发现传达给CPU2上的B. 您唯一的沟通是CPU自己正在做的事情.因此,如果他们无法在没有障碍的情况下确定之前/之后,那么问"如果B在A之后运行会发生什么,没有障碍" 会毫无意义.
顺便说一句,我不确定你在C#中有什么可用,但通常做了什么,Code样本#1真正需要的是写入时的单个释放障碍,以及读取时的单个获取障碍:
void A()
{
_answer = 123;
WriteWithReleaseBarrier(_complete, true); // "publish" values
}
void B()
{
if (ReadWithAcquire(_complete)) // subscribe
{
Console.WriteLine (_answer);
}
}
Run Code Online (Sandbox Code Playgroud)
单词"subscribe"并不常用于描述情况,但"发布"是.我建议你阅读Herb Sutter关于穿线的文章.
这将障碍放在恰当的位置.
对于代码示例#2,这实际上不是内存屏障问题,它是编译器优化问题 - 它保存complete在寄存器中.一个内存屏障会强制它出来,volatile但是可能会调用外部函数 - 如果编译器无法判断外部函数是否被修改complete,它将从内存中重新读取它.即可complete能将地址传递给某个函数(在编译器无法检查其详细信息的地方定义):
while (!complete)
{
some_external_function(&complete);
}
Run Code Online (Sandbox Code Playgroud)
即使函数没有修改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)
...
Run Code Online (Sandbox Code Playgroud)
如果没有屏障,CPU可能会将所有这5个读取请求"挂起":
a,b,c,d,_complete
Run Code Online (Sandbox Code Playgroud)
如果没有屏障,处理器可以重新排序这些请求以优化内存访问(即,如果_complete和'a'位于同一缓存行或其他内容上).
使用屏障,CPU从内存中获取a,b,c,d,然后将_complete作为请求放入.确保'b'(例如)在_complete之前读取 - 即没有重新排序.
问题是 - 它有什么不同?
如果a,b,c,d独立于_complete,则无关紧要.所有的障碍都是慢下来.所以,是的,稍后_complete再读.所以数据更新鲜.在读取之前将睡眠(100)或一些忙等待循环放在那里也会使它"更新鲜"!:-)
所以重点是 - 保持相对.是否需要在相对于其他一些数据之前/之后读/写数据?这就是问题所在.
并且没有放下文章的作者 - 他确实提到"如果B在A之后跑了......".目前尚不清楚他是否在想象A之后的B对代码至关重要,可以通过代码观察,或者只是无关紧要.
| 归档时间: |
|
| 查看次数: |
613 次 |
| 最近记录: |