使用C+中的内存屏障防止空气稀薄值溢出+; 让我们考虑C++中的以下两个线程并发程序:
使用C+中的内存屏障防止空气稀薄值溢出+; 让我们考虑C++中的以下两个线程并发程序:,c++,x86,memory-barriers,memory-model,C++,X86,Memory Barriers,Memory Model,x,y是全局变量,r1,r2是线程本地变量,store和loadtoint是原子变量。 内存模型=C++11 int x = 0, int y = 0 r1 = x | r2 = y y = r1 | x = r2 允许编译器将其编译为: int x = 0, int y = 0 r1 = x | r2 = 42 y = r1 | x = r2 | if(y != 42) | x = r2 = y 而且,虽然它是线程内一
x,y
是全局变量,r1,r2
是线程本地变量,store
和load
toint
是原子变量。
内存模型=C++11
int x = 0, int y = 0
r1 = x | r2 = y
y = r1 | x = r2
允许编译器将其编译为:
int x = 0, int y = 0
r1 = x | r2 = 42
y = r1 | x = r2
| if(y != 42)
| x = r2 = y
而且,虽然它是线程内一致的,但它可能会导致异常结果,因为该程序的执行可能会导致(x,y)=(42,42)
这就是所谓的空气稀薄值问题。它是存在的,我们必须接受它
我的问题是:内存障碍是否会阻止编译器进行导致空值的疯狂优化
例如:
[fence] = atomic_thread_fence(memory_order_seq_cst);
int x = 0, int y = 0
r1 = x | r2 = y
[fence] | [fence]
y = r1 | x = r2
不是靠它自己。在您的示例中,没有同步两个线程的内容。特别是,两个螺纹中的围栏不会导致螺纹在该点同步;例如,您可能会得到以下序列:
(Thread #1) | (Thread #2)
r1 = x |
[fence] |
y = junk temporary |
| r2 = y // junk!
| [fence]
| x = r2
y = r1 |
避免空气不足结果的最简单方法是使用原子整数:如果x和y原子,则它们不能具有“空气不足”值:
我的答案更详细地解释了C++轻松原子记忆模型的正式规则不排除“超薄”的值。但他们确实在一张便条中排除了它们这是一个问题,仅适用于使用mou-released
对程序进行正式验证,而不适用于实际实现。如果您避免未定义的行为(在本问题的代码中没有定义),即使是非原子变量也可以避免此问题
在
x
和y
上存在未定义的数据竞争行为,因为它们是非原子的
变量,所以C++11标准对允许发生的事情完全没有说明
对于没有正式内存模型的较旧语言标准来说,这一点很重要,在这种情况下,人们使用volatile
或plainint
和编译器+asm屏障执行线程,在这种情况下,行为可能取决于编译器的工作方式。但幸运的是,“碰巧在当前实现上工作”线程化的糟糕日子已经过去了
在这里,障碍是没有帮助的,没有任何东西可以创建同步;正如@davmac所解释的,没有什么需要在全球运营秩序中“排队”的障碍将屏障视为使当前线程等待其以前的部分或全部操作全局可见的操作;屏障不会直接与其他线程交互。
稀薄空气的价值观是一件事,可能会发生的结果,这种未定义的行为;编译器被允许对非原子变量进行软件值预测,并发明对无论如何肯定会被写入的对象的写入。如果有一个发布存储,或者一个宽松的存储+一个屏障,编译器可能不被允许在它之前发明写操作,因为这可能会创建 一般来说,从C++11语言的角度来看,您无法确保程序的安全(除了互斥锁或使用atomics手动滚动锁定以防止一个线程在另一个线程编写时读取
x
)
宽松的原子足以防止编译器在没有任何其他成本的情况下发明写操作。 如果你指望这个变量的其他用途得到积极优化,除了可能会打败自动矢量化之类的东西
atomic_int x = 0, y = 0
r1 = x.load(mo_relaxed) | r2 = y.load(mo_relaxed)
y.store(r1, mo_relaxed) | x.store(r2, mo_relaxed)
值预测可以在线程2从y
中看到该值之前,推测性地将r2
的未来值输入管道,但在软件或硬件确定预测是正确的之前,其他线程实际上无法看到该值。(那将是发明一种写作方式)
e、 g.允许线程2编译为
r2 = y.load(mo_relaxed);
if (r2 == 42) { // control dependency, not a data dependency
x.store(42, mo_relaxed);
} else {
x.store(r2, mo_relaxed);
}
但正如我所说,x=42代码>只有在非推测性(硬件或软件推测)时才能对其他线程可见,因此值预测不能发明其他线程可以看到的值。C++11标准保证原子
我不知道/想不出任何机制,在y.load
看到实际的42之前,其他线程实际上可以看到42
的存储。(即,加载存储对加载进行重新排序,然后使用后续的从属存储)。不过,我不认为C++标准正式保证了这一点。如果编译器能够证明在某些情况下,r2
将始终是42,甚至删除控件依赖项,那么可能真的会进行积极的线程间优化
获取加载或释放存储肯定足以阻止因果关系冲突。这不完全是mo_consume
,因为r2
用作一个值,而不是指针。您不需要使用seq_cst
原子加载/存储,是吗?如果你只是想防止空气稀薄,那么这比你需要的要贵得多。我认为发布存储或获取加载足以防止加载存储重新排序。(实际上,您只需要击败价值预测,因此任何正常的实现都可能是安全的,使用release,并且肯定是使用mou-consume
,但通过当前的实现,mou-acquire
)Err实际上relaxed
足以防止空值,因为编译器无法发明对原子的写入。在实际看到它之前,我正在考虑存储y
的正确值,这违反了因果关系。@PeterCordes不,您不需要seq_cst
atomic load/store,是的,我认为relaxed
就足够了。但为了保持示例的简单性,seq_cst给了您太多(而且成本太高)的信息,几乎让答案变得不有趣。当然,这是安全的。在C++11内存中,理论上,你可以,
r2 = y.load(mo_relaxed);
if (r2 == 42) { // control dependency, not a data dependency
x.store(42, mo_relaxed);
} else {
x.store(r2, mo_relaxed);
}