Lon*_*gLT 4 c++ atomic memory-barriers c++11 stdatomic
正如我从测试用例中看到的:https : //godbolt.org/z/K477q1
生成的程序集加载/存储原子放松与普通变量相同:ldr 和 str
那么,松弛原子变量和普通变量之间有什么区别吗?
不同之处在于,不能保证正常的加载/存储是无撕裂的,而轻松的原子读/写则是。此外,原子保证编译器不会以类似于volatile保证的方式重新排列或优化内存访问。
(C++11 之前,volatile是滚动你自己的原子的重要组成部分。但现在它已经过时了。它在实践中仍然有效,但从不推荐:何时将 volatile 与多线程一起使用? - 基本上从不。)
在大多数平台上,架构默认提供无撕裂的加载/存储(对于对齐int和long),因此如果加载和存储没有得到优化,它在 asm 中的效果相同。请参阅为什么在 x86 上对自然对齐的变量原子进行整数赋值?例如。在 C++ 中,您可以在源代码中表达如何访问内存,而不是依赖特定于体系结构的功能来使代码按预期工作。
如果您在 asm 中手写,那么当值保存在寄存器中与加载/存储到(共享)内存时,您的源代码已经确定了。在 C++ 中,告诉编译器何时可以/不能将值保持私有是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
放宽原子规则的(轻微)区别是什么?其中包括指向 Herb Sutter“原子武器”文章的链接,该文章也链接在此处:https : //herbsutter.com/2013/02/11/atomic-weapons-the-c-memory-model-and-modern-hardware /
另请参阅 Peter Cordes 的链接文章:https : //electronics.stackexchange.com/q/387181
以及有关 Linux 内核的相关文章:https : //lwn.net/Articles/793253/
没有撕裂只是您得到的一部分std::atomic<T>- 您还可以避免数据竞争未定义的行为。
实际上,这是一个很好的问题,当我开始学习并发时,我也问了同样的问题。
我会尽可能简单地回答,即使答案有点复杂。
从不同线程*读取和写入相同的非原子变量是未定义的行为 -不保证一个线程读取另一个线程写入的值。
使用原子变量解决了这个问题 - 通过使用原子,即使内存顺序放宽,所有线程也保证读取最新的写入值。
事实上,无论内存顺序如何,原子始终是线程安全的! 内存顺序不适用于原子 -> 它适用于非原子数据。
事情是这样的——如果你使用锁,你就不必考虑那些低级的事情。内存顺序用于需要同步非原子数据的无锁环境。
这是无锁算法的美妙之处,我们使用始终线程安全的原子操作,但我们使用内存顺序“小猪包装”这些操作来同步这些算法中使用的非原子数据。
例如,无锁链表。通常,无锁链表节点看起来像这样:
Node:
Atomic<Node*> next_node;
T non_atomic_data
Run Code Online (Sandbox Code Playgroud)
现在,假设我将一个新节点推入列表中。next_node始终是线程安全的,另一个线程将始终看到最新的原子值。但是谁允许其他线程看到 的正确值呢non_atomic_data?
没有人。
这是使用内存顺序的一个完美示例 - 我们next_node通过添加同步 值的内存顺序来“搭载”原子存储和加载non_atomic_data。
因此,当我们将新节点存储到列表中时,我们使用memory_order_release将非原子数据“推送”到主内存。当我们通过读取来读取新节点时next_node,我们使用memory_order_acquire然后从主内存中“拉”出非原子数据。这样我们就可以确保 和next_node始终non_atomic_data在线程间同步。
memory_order_relaxed不同步任何非原子数据,它仅同步其本身 - 原子变量。使用此功能时,开发人员可以假设原子变量不引用由写入原子变量的同一线程发布的任何非原子数据。换句话说,该原子变量不是非原子数组的索引,也不是指向非原子数据的指针,也不是指向某些非线程安全集合的迭代器。(使用宽松的原子存储和加载作为常量查找表或单独同步的索引就可以了。如果指向或索引的数据是由同一线程写入的,则仅需要 acq/rel 同步。)这比使用更强的内存顺序更快(至少在某些体系结构上),但可以在较少的情况下使用。
很好,但这还不是完整的答案。我说过内存顺序不用于原子。我是半撒谎的。
在宽松的内存顺序下,原子仍然是线程安全的。但它们有一个缺点——可以重新订购。看下面的代码片段:
a.store(1, std::memory_order_relaxed);
b.store(2, std::memory_order_relaxed);
Run Code Online (Sandbox Code Playgroud)
事实上,a.store可能会发生在之后 b.store。CPU 一直这样做,这称为乱序执行,它是 CPU 用于加速执行的优化技术之一。a并且b仍然是线程安全的,即使线程安全存储可能以相反的顺序发生。
现在,如果订单有意义的话会发生什么?许多无锁算法的正确性取决于原子操作的顺序。
内存顺序也用于防止重新排序。这就是为什么内存顺序如此复杂,因为它们同时做两件事。
memory_order_acquire告诉编译器和 CPU不要执行在它之后、之前发生的操作。
相似性, memory_order_release告诉编译器和CPU不要执行它之前的操作,在它之后的操作。
memory_order_relaxed告诉编译器/CPU 原子操作可以重新排序是可能的,类似地,只要有可能,非原子操作就会重新排序。
atomic<T> 约束优化器不假设值在同一线程中的访问之间保持不变。
atomic<T>也使得确保该对象被充分地对齐:例如,用于32位的ISA一些C ++实现具有alignof(int64_t) = 4但alignof(atomic<int64_t>) = 8使无锁64位操作。(例如,用于 32 位 x86 GNU/Linux 的 gcc)。在这种情况下,通常需要一个特殊的指令,编译器可能不会以其他方式使用,例如 ARMv8 32 位ldp负载对,或 x86 SSE2,movq xmm然后再跳转到整数 regs。
在ASM中对于大多数的ISA,纯负荷和纯店内自然对齐int和long是原子为免费的,所以atomic<T>与memory_order_relaxed 可编译为相同的ASM作为纯变量; 原子性(无撕裂)不需要任何特殊的汇编。例如:为什么在 x86 上对自然对齐的变量原子进行整数赋值? 根据周围的代码,编译器可能无法优化对非原子对象的任何访问,在这种情况下,代码生成在普通T和atomic<T>mo_relaxed之间将相同。
反之则不然:像在 asm 中编写 C++ 一样编写 C++ 一点也不安全。 在 C++ 中,多个线程同时访问同一个对象是数据竞争未定义行为,除非所有访问都是读取。
因此,根据“as-if”优化规则,C++ 编译器可以假设没有其他线程正在更改循环中的变量。如果bool done不是原子的,一个类似的循环while(!done) { }将编译成if(!done) infinite_loop;,将负载提升到循环之外。有关编译器 asm 输出的详细示例,请参阅多线程程序卡在优化模式但在 -O0 中正常运行。(在禁用优化的情况下进行编译非常类似于使每个对象volatile:内存与 C++ 语句之间的抽象机同步,以实现一致的调试。)
同样显然 RMW 操作像+=或var.fetch_add(1, mo_seq_cst)是原子的,并且必须编译为与非原子不同的 asm +=。 num++ 可以是“int num”的原子吗?
原子操作对优化器的约束类似于什么volatile。在实践中volatile是一种滚动自己的方式mo_relaxed atomic<T>,但没有任何简单的方法来获得订购 wrt。其他操作。某些编译器(例如 GCC)事实上支持它,因为 Linux 内核使用它。 但是,atomic<T>通过 ISO C++ 标准保证工作;什么时候在多线程中使用 volatile?-有几乎没有理由推出自己的,只是使用atomic<T>与mo_relaxed。
还相关:为什么编译器不合并冗余 std::atomic 写入?/编译器是否可以优化两个原子负载?- 编译器目前根本不优化原子,因此atomic<T>目前相当于volatile atomic<T>,等待进一步的标准工作,为程序员提供方法来控制何时/什么优化是可以的。
| 归档时间: |
|
| 查看次数: |
822 次 |
| 最近记录: |