6

我是否错误地假设 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 设为原子,重点是探索 atomic::load 的行为。

虽然我不得不承认我并不完全理解内存顺序的不同宽松语义的正式定义,但我认为顺序一致的顺序非常简单,因为它保证“存在一个单一的总顺序,其中所有线程都观察所有修改以相同的顺序。” 对我来说,这意味着具有 std::memory_order_seq_cst 的默认内存顺序的 std::atomic::load 也将充当内存围栏。以下“顺序一致的排序”下的声明进一步证实了这一点:

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

4

2 回答 2

3

唉,评论太长了:

原子的意思不是“似乎对系统的其余部分立即发生”吗?

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

对于读取原子对象 M 的值的原子操作 B,如果在 B 之前有一个 memory_order_seq_cst 栅栏 X,则 B 观察总顺序 S 中 M 在 X 之前的最后一个 memory_order_seq_cst 修改或后面的修改M 在其修改顺序中。

基本上,SEQ_CST就像标准所说的那样强制全局顺序,但是读取可以返回旧值而不违反“原子”约束。

要完成“获取绝对最新值”,您需要执行强制硬件一致性协议锁定的lock操作(x86_64 上的指令)。如果您查看汇编输出,这就是原子比较和交换操作所做的事情。

于 2015-03-04T02:39:17.237 回答
2

我是否错误地假设 atomic::load 也应该充当内存屏障以确保所有以前的非原子写入都将被其他线程看到?

是的。atomic::load(SEQ_CST)只是强制读取不能加载“无效”值,并且编译器或围绕该语句的 cpu不能重新排序写入和加载。这并不意味着您将始终获得最新的价值。

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

对于 Thread1 看不到 Thread2 的写入,因此 not set triggered,以及 Thread2 看不到 Thread1 的写入(同样,不是 setting triggered),它完全有效,因为您只能从一个线程“原子地”写入。

对于两个线程写入和读取共享值,您需要在每个线程中设置一个屏障以保持一致性。看起来您已经根据您的代码注释知道这一点,所以我将把它留在“C++ 标准在准确描述原子/多线程操作的含义时有些误导”。

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

不确定我是否解释得很好,但如果您愿意,我很乐意提供更多详细信息。

于 2015-03-01T17:00:05.773 回答