微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

加载/存储松弛原子和普通变量之间有什么区别?

如何解决加载/存储松弛原子和普通变量之间有什么区别?

我从一个测试用例中看到:https://godbolt.org/z/K477q1

生成的程序集加载/存储原子松弛与普通变量相同:ldr和str

那么,原子变量和普通变量之间有什么区别吗?

解决方法

区别在于,不能正常加载/存储 保证 没有撕裂,而宽松的原子读/写则可以。另外,原子保证编译器不会以与volatile保证的类似方式重新安排或优化内存访问。

(C ++ 11以前的版本,volatile是滚动您自己的原子的重要部分。但是,现在它已经过时了。它在实践中仍然有效,但是从不推荐使用:When to use volatile with multi threading? -基本上不会。)

在大多数平台上,碰巧该体系结构默认情况下会提供无撕裂的加载/存储(对于对齐的intlong),因此在asm if 加载和存储不会得到优化。例如,请参见Why is integer assignment on a naturally aligned variable atomic on 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


链接以供进一步阅读:


另请参阅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

现在,假设我将一个新节点推送到列表中。 next_node始终是线程安全的,另一个线程将始终看到最新的原子值。 但是谁授予其他线程看到non_atomic_data的正确值?

没有人。

这是使用内存顺序的一个完美示例-我们还通过添加同步next_node的内存顺序来“搭载”原子存储并加载到non_atomic_data

因此,当我们将新节点存储到列表时,我们使用memory_order_release将非原子数据“推送”到主内存。当我们通过读取next_node来读取新节点时,我们使用memory_order_acquire,然后从主内存中“拉出”非原子数据。 这样,我们可以确保next_nodenon_atomic_data始终在线程之间同步。

memory_order_relaxed不同步任何非原子数据,它仅同步自身-原子变量。使用此方法时,开发人员可以假定原子变量未引用由编写原子变量的同一线程发布的任何非原子数据。换句话说,该原子变量不是例如非原子数组的索引,非原子数据的指针或某些非线程安全集合的迭代器。 (最好使用宽松的原子存储并将索引的负载装入恒定查找表或单独同步的表中。如果指向或索引的数据是由同一线程写入的,则仅需要acq / rel同步。) 这比使用更强的内存顺序要快(至少在某些架构上),但是可以在更少的情况下使用。

太好了,但这还不是完整的答案。我说过内存顺序不用于原子。我在躺着。

通过宽松的内存顺序,原子仍然是线程安全的。但它们有一个缺点-可以重新排序。查看以下代码段:

a.store(1,std::memory_order_relaxed);
b.store(2,std::memory_order_relaxed);

实际上,a.store可能发生在之后 b.store。 CPU一直在执行此操作,称为乱序执行,它是CPU用于加速执行的优化技术之一。 ab仍然是线程安全的,即使线程安全存储可能以相反的顺序发生。

现在,如果订单含义明确,该怎么办?许多无锁算法的正确性都取决于原子操作的顺序。

内存命令也用于防止重新排序。这就是内存顺序如此复杂的原因,因为它们同时执行两项操作。

memory_order_acquire告诉编译器和CPU 不要执行在代码级之后,之前发生的操作。

相似性,memory_order_release告诉编译器和CPU 不要执行在代码之前,之后的操作。

memory_order_relaxed告诉编译器/ CPU,可以对原子操作进行重新排序,这与非原子操作尽可能地重新排序类似。

,

atomic<T>约束优化器不要假设同一线程中两次访问之间的值不变。

atomic<T>还可以确保对象充分对齐:例如某些32位ISA的C ++实现具有alignof(int64_t) = 4alignof(atomic<int64_t>) = 8可以启用无锁的64位操作。 (例如,用于32位x86 GNU / Linux的gcc)。在那种情况下,通常需要一条特殊的指令,否则编译器可能不会使用它,例如:跳至整数寄存器之前,ARMv8 32位ldp负载对或x86 SSE2 movq xmm


在大多数ISA的asm中,自然对齐的intlong的纯负载和纯存储是免费的原子操作,因此atomic<T>memory_order_relaxed 可以编译为与普通变量相同的asm;原子性(不撕裂)不需要任何特殊的组件。例如:Why is integer assignment on a naturally aligned variable atomic on x86?根据周围的代码,编译器可能无法设法优化对非原子对象的任何访问,在这种情况下,普通T和{{1 }},且具有mo_relaxed。

事实并非如此:就像在asm中编写C ++一样,不是是绝对安全的。在C ++中,多个线程在同一位置访问同一对象。除非所有访问都被读取,否则数据种族的行为是不确定的。

因此,C ++编译器可以假定没有其他线程在循环per the "as-if" optimization rule中更改变量。如果atomic<T>不是原子的,则类似bool done的循环将编译到while(!done) { }中,从而减轻了循环的负担。有关编译器asm输出的详细示例,请参见Multithreading program stuck in optimized mode but runs normally in -O0。 (使用optimization disabled进行编译非常类似于使每个对象if(!done) infinite_loop;:内存与C ++语句之间的抽象机同步以进行一致的调试。)


显然,volatile+=之类的RMW操作是原子性的,并且必须编译为与非原子性var.fetch_add(1,mo_seq_cst)不同的asm。 Can num++ be atomic for 'int num'?


原子操作对优化器的约束类似于+=的约束。实际上,volatile是滚动自己的volatile mo_relaxed的一种方法,但是没有任何简单的方法来订购wrt。其他操作。实际上它在某些编译器(例如GCC)上受支持,因为它由Linux内核使用。 但是atomic<T>可以通过ISO C ++标准来保证运行; When to use volatile with multi threading?-几乎没有理由自己动手,只需将atomic<T>atomic<T>一起使用。

也相关:Why don't compilers merge redundant std::atomic writes? / Can and does the compiler optimize out two atomic loads?-编译器目前根本不优化原子,因此mo_relaxed当前等效于atomic<T>,有待进一步的标准来提供程序员控制何时/什么优化可行的方法。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。