std :: atomic :: load的内存排序行为

时间:2015-02-28 15:36:09

标签: c++ c++11 atomic atomicity memory-fences

我错误地认为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行为的理解产生了疑问。

2 个答案:

答案 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 ++,我认为它仍然是最好的,可以考虑您在底层架构上所做的工作。

我不确定我是否解释得那么好,但如果您愿意,我会很乐意详细说明。