我正在阅读这篇文章Memory Ordering at Compile Time,其中说:
事实上,大多数函数调用充当编译器障碍, 它们是否包含自己的编译器屏障。这排除了内联函数,使用pure属性声明的函数以及使用链接时代码生成的情况。除了这些情况之外,对外部函数的调用甚至比编译器障碍更强,因为编译器不知道函数的副作用是什么。
这是真实的陈述吗?想想这个样本 -
std::atomic_bool flag = false;
int value = 0;
void th1 () { // running in thread 1
value = 1;
// use atomic & release to prevent above sentence being reordered below
flag.store(true, std::memory_order_release);
}
void th2 () { // running in thread 2
// use atomic & acquire to prevent asset(..) being reordered above
while (!flag.load(std::memory_order_acquire)) {}
assert (value == 1); // should never fail!
}
然后我们可以删除原子但用函数调用替换 -
bool flag = false;
int value = 0;
void writeflag () {
flag = true;
}
void readflag () {
while (!flag) {}
}
void th1 () {
value = 1;
writeflag(); // would function call prevent reordering?
}
void th2 () {
readflag(); // would function call prevent reordering?
assert (value == 1); // would this fail???
}
有什么想法吗?
答案 0 :(得分:2)
编译器障碍与内存障碍不同。编译器障碍阻止编译器跨越障碍移动代码。内存障碍(松散地说)阻止硬件跨屏障移动读写。对于原子,你需要两者,你还需要确保在读或写时不会撕裂值。
答案 1 :(得分:1)
正式,不,如果仅因为链接时代码生成是有效的实现选择而且不必是可选的。
还有第二次疏忽,以及逃脱分析。声称是"编译器不知道函数的副作用是什么。" ,但是如果没有指向我的局部变量的指针从我的函数,然后编译器确实知道没有其他函数更改它们。
答案 2 :(得分:1)
您混淆了用于线程间内存可见性的内存屏障和编译器屏障,后者不是线程设备,只是防止编译器副作用重新排序的设备(或技巧) >。
您的线程示例需要一个内存屏障。
您可以使用编译器屏障来确保以给定的顺序(在本地CPU上)执行内存方面的工作,以用于其他目的,例如基准测试,解决类型别名冲突,集成汇编代码或信号处理(用于仅在同一线程中处理的信号)。
答案 3 :(得分:0)
在第二个例子中,即使我们假设没有任何类型的重新排序,行为也是未定义的。
来自变量标志的写入和读取不是原子的,并且存在竞争条件 1 。没有重新排序并不能保证两个线程不会同时访问变量单元。当一个线程在函数readflag中触发while循环并读取标志,而另一个线程在writeflag中写入标志时,就会发生这种情况。
1 (引自:ISO / IEC 14882:2011(E)1.10多线程执行和数据竞赛21)
如果程序在不同的线程中包含两个冲突的动作,则程序的执行包含数据竞争,
其中至少有一个不是原子的,也不会发生在另一个之前。任何此类数据竞赛都会产生
未定义的行为