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

Memory ordering behavior of std::atomic::load

我是否错误地假设 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;

我预计在执行两个 'triggered' 之后都是正确的。请不要建议使 arm1 原子化,重点是探索 atomic::load.

的行为

虽然我不得不承认我不完全理解 memory order 的不同宽松语义的正式定义,但我认为 顺序一致的顺序 非常简单因为它保证 "a single total order exists in which all threads observe all modifications in the same order." 对我来说,这意味着默认内存顺序为 std::memory_order_seq_cst 的 std::atomic::load 也将充当内存栅栏。 "Sequentially-consistent ordering" 下的以下陈述进一步证实了这一点:

总顺序排序需要在所有多核系统上使用完整的内存栅栏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 的行为的理解产生了疑问。

Am I wrong to assume that the atomic::load should also act as a memory barrier ensuring that all previous non-atomic writes will become visible by other threads?

是的。 atomic::load(SEQ_CST) 只是强制读取不能加载 'invalid' 值,编译器 或周围的 cpu 可能不会对写入和加载进行重新排序陈述。这并不意味着您将始终获得最新的价值。

我希望您的代码会出现数据竞争,因为屏障并不能确保在给定时间看到最新的值,它们只是阻止重新排序。

对于 Thread1 看不到 Thread2 的写入并因此不设置 triggered 以及 Thread2 看不到 Thread1 的写入(同样,不设置 triggered)是完全有效的,因为你只从一个线程写 'atomically'。

在两个线程写入和读取共享值的情况下,您需要在每个线程中设置一个屏障来保持一致性。看起来您已经在代码注释中知道了这一点,所以我将其保留在 "the C++ standard is somewhat misleading when it comes to accurately describing meaning of atomic / multithreaded operations"。

即使您正在编写 C++,在我看来,最好还是考虑一下您在底层架构上所做的事情。

不确定我是否解释得很好,但如果您愿意,我很乐意详细介绍。

唉,评论太长了:

Isn't the meaning of atomic "to appear to occur instantaneously to the rest of the system"?

我会说是和不是那个,这取决于你怎么想。对于 SEQ_CST 的写入,是的。但就如何处理原子负载而言,请查看 C++11 标准的 29.3。具体来说,29.3.3 非常适合阅读,而 29.3.4 可能正是您要查找的内容:

For an atomic operation B that reads the value of an atomic object M, if there is a memory_order_seq_- cst fence X sequenced before B, then B observes either the last memory_order_seq_cst modification of M preceding X in the total order S or a later modification of M in its modification order.

基本上,SEQ_CST 强制全局顺序就像标准所说的那样,但读取可以 return 和旧值而不违反 'atomic' 约束。

要完成 'getting the absolute latest value',您需要执行强制硬件一致性协议锁定的操作(x86_64 上的 lock 指令)。如果您查看程序集输出,这就是原子比较和交换操作所做的。