load/store 松弛原子变量和普通变量有什么区别?
What is the difference between load/store relaxed atomic and normal variable?
正如我从测试用例中看到的:https://godbolt.org/z/K477q1
生成的程序集load/storeatomic relaxed与普通变量一样:ldr和str
那么,松弛原子变量和普通变量有什么区别吗?
区别在于正常的load/store不是保证是tear-free,而宽松的atomic read/write 是。此外,原子保证编译器不会以类似于 volatile
保证的方式重新排列或 optimise-out 内存访问。
(Pre-C++11, volatile
是滚动你自己的原子的重要部分。但现在它已经过时了。它在实践中仍然有效,但从不推荐:When to use volatile with multi threading? - 基本上从来没有。)
在大多数平台上,架构默认提供 tear-free load/store(对齐 int
和 long
),所以它在asm if 加载和存储没有得到优化。例如,参见 。在 C++ 中,由您来表达应如何在源代码中访问内存,而不是依赖 architecture-specific 功能来使代码按预期工作。
如果您是 hand-writing 在 asm 中,当值保存在寄存器中与加载/存储到(共享)内存时,您的源代码已经确定了。在 C++ 中,告诉编译器何时 can/can 不将值保持私有是 std::atomic<T>
存在的部分原因。
如果您阅读了关于此主题的 一篇 文章,请查看此处的 Preshing:
https://preshing.com/20130618/atomic-vs-non-atomic-operations/
也可以试试 CppCon 2017 上的这个演示文稿:
https://www.youtube.com/watch?v=ZQFzMfHIxng
进一步阅读的链接:
https://en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering
Causing non-atomics to tear
-
What is the (slight) difference on the relaxing atomic rules? 其中包括一篇 link 到 Herb Sutter 的“原子武器”文章,该文章也在此处 link 编辑:
https://herbsutter.com/2013/02/11/atomic-weapons-the-c-memory-model-and-modern-hardware/
另见 Peter Cordes 的 linked 文章:https://electronics.stackexchange.com/q/387181
还有一个关于 Linux 内核的相关信息:https://lwn.net/Articles/793253/
没有撕裂只是您使用 std::atomic<T>
获得的一部分 - 您还可以避免数据竞争未定义的行为。
这个问题其实很好,我开始学习并发的时候也问过同样的问题。
我会尽可能简单地回答,即使答案有点复杂。
从不同线程*读取和写入同一个非原子变量是未定义的行为——一个线程是 not 保证读取另一个线程写入的值。
使用原子变量解决了这个问题 - 通过使用原子,所有线程都保证读取最新的 writen-value 即使内存顺序放宽了.
事实上,无论内存顺序如何,原子 始终是线程安全的!
内存顺序不适用于原子 -> 它适用于 非原子数据.
事情是这样的——如果你使用锁,你就不必考虑那些 low-level 事情。内存顺序用于 lock-free 环境,我们需要同步 非原子数据。
这是无锁算法的美妙之处,我们使用始终线程安全的原子操作,但我们“piggy-pack”那些具有内存顺序的操作以同步那些算法中使用的非原子数据。
例如,lock-free linked 列表。通常,lock-free link 列表节点看起来像这样:
Node:
Atomic<Node*> next_node;
T non_atomic_data
现在,假设我将一个新节点推入列表。 next_node
始终是线程安全的,另一个线程将总是 看到最新的原子值。
但是谁允许其他线程看到 non_atomic_data
的正确值?
No-one.
这是使用内存顺序的一个完美示例 - 我们通过添加同步 non_atomic_data
.[=29 的值的内存顺序来“搭载”原子存储和加载到 next_node
=]
所以当我们向列表中存储一个新节点时,我们使用memory_order_release
将非原子数据“推送”到主内存。当我们通过读取 next_node
读取新节点时,我们使用 memory_order_acquire
然后我们从主内存中“拉出”非原子数据。
这样我们可以确保 next_node
和 non_atomic_data
始终跨线程同步。
memory_order_relaxed
不同步任何 non-atomic 数据,它只同步自己 - 原子变量。使用它时,开发人员可以假设原子变量不引用由编写原子变量的同一线程发布的任何 non-atomic 数据。换句话说,该原子变量不是 non-atomic 数组的索引,也不是指向非原子数据的指针,也不是指向某些 non-thread 安全集合的迭代器。 (使用宽松的原子存储和加载索引到常量查找 table 或单独同步的索引会很好。如果 pointed-to 或索引数据,则只需要 acq/rel 同步由同一个线程编写。)
这比使用更强的内存顺序更快(至少在某些体系结构上),但可以在更少的情况下使用。
很好,但这还不是完整的答案。我说过内存顺序不用于原子。我是 half-lying.
在宽松的内存顺序下,原子仍然是线程安全的。但它们有一个缺点 - 它们可能是 re-ordered。看看下面的片段:
a.store(1, std::memory_order_relaxed);
b.store(2, std::memory_order_relaxed);
实际上,a.store
可能发生在 b.store
之后。 CPU 一直这样做,它被称为 乱序执行 并且它是 CPU 用来加速执行的优化技术之一。 a
和 b
仍然是 thread-safe,即使 thread-safe 存储可能以相反的顺序发生。
现在,如果订单有意义会怎样?许多 lock-free 算法的正确性取决于原子操作的顺序。
内存顺序也用于防止重新排序。这就是内存指令如此复杂的原因,因为它们同时做 2 件事。
memory_order_acquire
告诉编译器和 CPU 不要执行在它 code-wise 之后,在它 .[=29= 之前发生的操作]
相似性,memory_order_release
告诉编译器和CPU 不要执行它之前code-wise,之后.[=29的操作=]
memory_order_relaxed
告诉 compiler/cpu 原子操作可以 re-ordered 是可能的,以类似的方式尽可能重新排序非原子操作。
atomic<T>
约束优化器不假定同一线程中的访问之间的值不变。
atomic<T>
还确保对象充分对齐:例如一些 32 位 ISA 的 C++ 实现具有 alignof(int64_t) = 4
但 alignof(atomic<int64_t>) = 8
以启用 lock-free 64 位操作。 (例如 32 位 x86 GNU/Linux 的 gcc)。在这种情况下,通常需要编译器可能不会使用的特殊指令,例如ARMv8 32 位 ldp
load-pair,或 x86 SSE2 movq xmm
在跳转到整数 regs 之前。
在大多数 ISA 的 asm 中,naturally-aligned 的 pure-load 和 pure-store int
和 long
是免费的原子,因此 atomic<T>
with memory_order_relaxed
can 编译成与普通变量相同的 asm;原子性(无撕裂)不需要任何特殊的 asm。例如: 根据周围的代码,编译器可能无法优化对 non-atomic 对象的任何访问,在这种情况下 code-gen 与普通 T
之间是相同的atomic<T>
和 mo_relaxed.
反之则不然:不像用 asm 编写 C++ 一样安全。 在 C++ 中,多个线程同时访问同一个对象是 data-race 未定义的行为,除非所有访问都是读取。
因此允许 C++ 编译器假设没有其他线程在循环中更改变量,per the "as-if" optimization rule. If bool done
is not atomic, a loop like while(!done) { }
will compile into if(!done) infinite_loop;
, hoisting the load out of the loop. See for a detailed example with compiler asm output. (Compiling with optimization disabled 非常类似于使每个对象 volatile
:内存与抽象机之间同步用于一致调试的 C++ 语句。)
也很明显,像 +=
或 var.fetch_add(1, mo_seq_cst)
这样的 RMW 操作是原子的 并且必须编译成与 non-atomic [=26 不同的 asm =].
原子操作对优化器的约束类似于volatile
所做的。在实践中 volatile
是一种推出自己的 mo_relaxed
atomic<T>
的方法,但没有任何简单的方法来订购 wrt。其他操作。 de-facto 某些编译器支持它,比如 GCC,因为它被 Linux 内核使用。 但是,atomic<T>
保证按照ISO C++标准工作; When to use volatile with multi threading? - 几乎没有理由自己推出,只需使用 atomic<T>
和 mo_relaxed
。
也相关: / - 编译器目前根本不优化原子,所以 atomic<T>
目前等同于 volatile atomic<T>
,等待进一步的标准工作为程序员提供方法控制什么时候/什么优化是可以的。
正如我从测试用例中看到的:https://godbolt.org/z/K477q1
生成的程序集load/storeatomic relaxed与普通变量一样:ldr和str
那么,松弛原子变量和普通变量有什么区别吗?
区别在于正常的load/store不是保证是tear-free,而宽松的atomic read/write 是。此外,原子保证编译器不会以类似于 volatile
保证的方式重新排列或 optimise-out 内存访问。
(Pre-C++11, volatile
是滚动你自己的原子的重要部分。但现在它已经过时了。它在实践中仍然有效,但从不推荐:When to use volatile with multi threading? - 基本上从来没有。)
在大多数平台上,架构默认提供 tear-free load/store(对齐 int
和 long
),所以它在asm if 加载和存储没有得到优化。例如,参见
如果您是 hand-writing 在 asm 中,当值保存在寄存器中与加载/存储到(共享)内存时,您的源代码已经确定了。在 C++ 中,告诉编译器何时 can/can 不将值保持私有是 std::atomic<T>
存在的部分原因。
如果您阅读了关于此主题的 一篇 文章,请查看此处的 Preshing: https://preshing.com/20130618/atomic-vs-non-atomic-operations/
也可以试试 CppCon 2017 上的这个演示文稿: https://www.youtube.com/watch?v=ZQFzMfHIxng
进一步阅读的链接:
https://en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering
Causing non-atomics to tear
What is the (slight) difference on the relaxing atomic rules? 其中包括一篇 link 到 Herb Sutter 的“原子武器”文章,该文章也在此处 link 编辑: https://herbsutter.com/2013/02/11/atomic-weapons-the-c-memory-model-and-modern-hardware/
另见 Peter Cordes 的 linked 文章:https://electronics.stackexchange.com/q/387181
还有一个关于 Linux 内核的相关信息:https://lwn.net/Articles/793253/
没有撕裂只是您使用 std::atomic<T>
获得的一部分 - 您还可以避免数据竞争未定义的行为。
这个问题其实很好,我开始学习并发的时候也问过同样的问题。
我会尽可能简单地回答,即使答案有点复杂。
从不同线程*读取和写入同一个非原子变量是未定义的行为——一个线程是 not 保证读取另一个线程写入的值。
使用原子变量解决了这个问题 - 通过使用原子,所有线程都保证读取最新的 writen-value 即使内存顺序放宽了.
事实上,无论内存顺序如何,原子 始终是线程安全的! 内存顺序不适用于原子 -> 它适用于 非原子数据.
事情是这样的——如果你使用锁,你就不必考虑那些 low-level 事情。内存顺序用于 lock-free 环境,我们需要同步 非原子数据。
这是无锁算法的美妙之处,我们使用始终线程安全的原子操作,但我们“piggy-pack”那些具有内存顺序的操作以同步那些算法中使用的非原子数据。
例如,lock-free linked 列表。通常,lock-free link 列表节点看起来像这样:
Node:
Atomic<Node*> next_node;
T non_atomic_data
现在,假设我将一个新节点推入列表。 next_node
始终是线程安全的,另一个线程将总是 看到最新的原子值。
但是谁允许其他线程看到 non_atomic_data
的正确值?
No-one.
这是使用内存顺序的一个完美示例 - 我们通过添加同步 non_atomic_data
.[=29 的值的内存顺序来“搭载”原子存储和加载到 next_node
=]
所以当我们向列表中存储一个新节点时,我们使用memory_order_release
将非原子数据“推送”到主内存。当我们通过读取 next_node
读取新节点时,我们使用 memory_order_acquire
然后我们从主内存中“拉出”非原子数据。
这样我们可以确保 next_node
和 non_atomic_data
始终跨线程同步。
memory_order_relaxed
不同步任何 non-atomic 数据,它只同步自己 - 原子变量。使用它时,开发人员可以假设原子变量不引用由编写原子变量的同一线程发布的任何 non-atomic 数据。换句话说,该原子变量不是 non-atomic 数组的索引,也不是指向非原子数据的指针,也不是指向某些 non-thread 安全集合的迭代器。 (使用宽松的原子存储和加载索引到常量查找 table 或单独同步的索引会很好。如果 pointed-to 或索引数据,则只需要 acq/rel 同步由同一个线程编写。)
这比使用更强的内存顺序更快(至少在某些体系结构上),但可以在更少的情况下使用。
很好,但这还不是完整的答案。我说过内存顺序不用于原子。我是 half-lying.
在宽松的内存顺序下,原子仍然是线程安全的。但它们有一个缺点 - 它们可能是 re-ordered。看看下面的片段:
a.store(1, std::memory_order_relaxed);
b.store(2, std::memory_order_relaxed);
实际上,a.store
可能发生在 b.store
之后。 CPU 一直这样做,它被称为 乱序执行 并且它是 CPU 用来加速执行的优化技术之一。 a
和 b
仍然是 thread-safe,即使 thread-safe 存储可能以相反的顺序发生。
现在,如果订单有意义会怎样?许多 lock-free 算法的正确性取决于原子操作的顺序。
内存顺序也用于防止重新排序。这就是内存指令如此复杂的原因,因为它们同时做 2 件事。
memory_order_acquire
告诉编译器和 CPU 不要执行在它 code-wise 之后,在它 .[=29= 之前发生的操作]
相似性,memory_order_release
告诉编译器和CPU 不要执行它之前code-wise,之后.[=29的操作=]
memory_order_relaxed
告诉 compiler/cpu 原子操作可以 re-ordered 是可能的,以类似的方式尽可能重新排序非原子操作。
atomic<T>
约束优化器不假定同一线程中的访问之间的值不变。
atomic<T>
还确保对象充分对齐:例如一些 32 位 ISA 的 C++ 实现具有 alignof(int64_t) = 4
但 alignof(atomic<int64_t>) = 8
以启用 lock-free 64 位操作。 (例如 32 位 x86 GNU/Linux 的 gcc)。在这种情况下,通常需要编译器可能不会使用的特殊指令,例如ARMv8 32 位 ldp
load-pair,或 x86 SSE2 movq xmm
在跳转到整数 regs 之前。
在大多数 ISA 的 asm 中,naturally-aligned 的 pure-load 和 pure-store int
和 long
是免费的原子,因此 atomic<T>
with memory_order_relaxed
can 编译成与普通变量相同的 asm;原子性(无撕裂)不需要任何特殊的 asm。例如:T
之间是相同的atomic<T>
和 mo_relaxed.
反之则不然:不像用 asm 编写 C++ 一样安全。 在 C++ 中,多个线程同时访问同一个对象是 data-race 未定义的行为,除非所有访问都是读取。
因此允许 C++ 编译器假设没有其他线程在循环中更改变量,per the "as-if" optimization rule. If bool done
is not atomic, a loop like while(!done) { }
will compile into if(!done) infinite_loop;
, hoisting the load out of the loop. See volatile
:内存与抽象机之间同步用于一致调试的 C++ 语句。)
也很明显,像 +=
或 var.fetch_add(1, mo_seq_cst)
这样的 RMW 操作是原子的 并且必须编译成与 non-atomic [=26 不同的 asm =].
原子操作对优化器的约束类似于volatile
所做的。在实践中 volatile
是一种推出自己的 mo_relaxed
atomic<T>
的方法,但没有任何简单的方法来订购 wrt。其他操作。 de-facto 某些编译器支持它,比如 GCC,因为它被 Linux 内核使用。 但是,atomic<T>
保证按照ISO C++标准工作; When to use volatile with multi threading? - 几乎没有理由自己推出,只需使用 atomic<T>
和 mo_relaxed
。
也相关:atomic<T>
目前等同于 volatile atomic<T>
,等待进一步的标准工作为程序员提供方法控制什么时候/什么优化是可以的。