我错误地认为atomic :: load也应该充当内存屏障,确保所有以前的非原子写入将被其他线程看到?
举例说明:
volatile bool arm1 = false;
std::atomic_bool arm2 = false;
bool triggered = false;
线程1:
arm1 = true;
//std::std::atomic_thread_fence(std::memory_order_seq_cst); // this would do the trick
if (arm2.load())
triggered = true;
线程2:
arm2.store(true);
if (arm1)
triggered = true;
我预计在执行两个“触发”之后是真的。请不要建议使arm1原子,重点是探索原子:: load的行为。
虽然我不得不承认我不完全理解memory order的不同轻松语义的正式定义我认为顺序一致排序非常简单明了保证"存在单个总订单,其中所有线程以相同的顺序观察所有修改。"对我来说,这意味着std :: atomic :: load与默认内存顺序std :: memory_order_seq_cst也将充当内存栅栏。在"顺序一致排序"
下的以下陈述进一步证实了这一点总顺序排序需要在所有多核系统上使用完整的内存屏障CPU指令。
然而,我在下面的简单示例演示了MSVC 2013,gcc 4.9(x86)和clang 3.5.1(x86)的情况并非如此,其中原子载荷只是转换为加载指令。
#include <atomic>
std::atomic_long al;
#ifdef _WIN32
__declspec(noinline)
#else
__attribute__((noinline))
#endif
long load() {
return al.load(std::memory_order_seq_cst);
}
int main(int argc, char* argv[]) {
long r = load();
}
使用gcc,这看起来像:
load():
mov rax, QWORD PTR al[rip] ; <--- plain load here, no fence or xchg
ret
main:
call load()
xor eax, eax
ret
我省略了基本相同的msvc和clang。现在,在ARM的gcc上,我们得到了我的预期:
load():
dmb sy ; <---- data memory barrier here
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
ldr r0, [r3]
dmb sy ; <----- and here
bx lr
main:
push {r3, lr}
bl load()
movs r0, #0
pop {r3, pc}
这不是一个学术问题,它导致我们的代码中存在一种微妙的竞争条件,这使我对std :: atomic行为的理解产生了疑问。
答案 0 :(得分:3)
叹息,这个评论太长了:
原子的含义“不是偶然发生在系统的其他部分”吗?
我会对那个说“是”和“否”,这取决于你如何看待它。对于SEQ_CST
的写入,是的。但就原子载荷的处理方式而言,请查看C ++ 11标准的29.3。具体来说,29.3.3是非常好的阅读,29.3.4可能是您正在寻找的具体内容:
对于读取原子对象M的值的原子操作B,如果有memory_order_seq_- cst fence X在B之前排序,然后B观察M的最后一次memory_order_seq_cst修改 在总顺序S中的X之前或在其修改顺序中的M的后续修改。
基本上,SEQ_CST
强制一个全局顺序就像标准所说的那样,但是读取可以返回旧值而不违反“原子”约束。
要完成'获取绝对最新值',您需要执行强制硬件一致性协议锁定(x86_64上的lock
指令)的操作。如果查看汇编输出,这就是原子比较和交换操作的作用。
答案 1 :(得分:2)
我错误地认为atomic :: load也应该充当内存屏障,确保所有先前的非原子写入都会被其他线程看到?
是。 atomic::load(SEQ_CST)
只强制读取无法加载“无效”值,并且编译器或cpu 可以对该语句进行重新排序。这并不意味着您将始终获得最新的价值。
我希望您的代码能够进行数据竞争,因为障碍并不能确保在给定时间看到最新值,它们只是阻止了重新排序。
完全有效的Thread1没有看到Thread2的写入因此没有设置triggered
,并且Thread2没有看到Thread1的写入(再次,没有设置triggered
),因为你只有从一个线程写'原子'。
使用两个线程编写和读取共享值,每个线程中都需要一个屏障来保持一致性。看起来你已经知道这已经基于你的代码注释了,所以我只想把它放在“C ++标准在准确描述原子/多线程操作的含义时有点误导”。
即使您正在编写C ++,我认为它仍然是最好的,可以考虑您在底层架构上所做的工作。
我不确定我是否解释得那么好,但如果您愿意,我会很乐意详细说明。