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
指令)。如果您查看程序集输出,这就是原子比较和交换操作所做的。
我是否错误地假设 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
指令)。如果您查看程序集输出,这就是原子比较和交换操作所做的。