C++中的std::atomic,什么是std::memory_order以及如何使用它们?

90

5
+1 非常好的问题。让我们期待一些好的答案! - Cheers and hth. - Alf
2
我曾经发现这个(相当密集的)答案对于介绍内存顺序非常有帮助。 - Luc Danton
似乎这个主题可以深入探讨。正如zvrba建议的那样,作为像我这样的初学者,使用默认值"sequential-consistent ordering"可能是一个不错的选择。 - 2607
相关链接:https://dev59.com/XWct5IYBdhLWcg3wHJzF - Ciro Santilli OurBigBook.com
5个回答

56
“std::memory_order” 值允许您在原子操作中指定细粒度内存排序的约束条件。如果您从多个线程修改和访问原子变量,那么将“std::memory_order”值传递给您的操作可以放宽编译器和处理器关于这些原子变量上的操作在何时对其他线程可见以及这些操作对应用程序中的非原子数据产生的同步效果的顺序约束。
默认的“std::memory_order_seq_cst”排序是最受限制的,并提供您可能期望的“直观”属性:如果线程 A 存储一些数据,然后使用“std::memory_order_seq_cst”设置原子标志,则如果线程 B 看到标志已设置,则它可以看到由线程 A 写入的数据。其他内存排序值不一定提供此保证,因此必须非常小心地使用。
基本前提是:除非您真的真的知道自己在做什么,并且可以证明放松使用在所有情况下都是安全的,否则不要使用除std::memory_order_seq_cst(默认值)以外的任何东西,并且(b)您的分析工具证明您打算使用放松排序的数据结构和操作是瓶颈。

我的书《C++ Concurrency in Action》专门介绍了C++内存模型、原子操作和std::memory_order约束的详细信息,并进一步介绍了使用原子操作进行同步的无锁数据结构以及放松排序约束的后果。

我关于互斥的Dekker's algorithmPeterson's algorithm的博客文章演示了其中的一些问题。


2
Relaxed对于它真正需要的东西来说非常安全——读取没有依赖/对应值且因此不存在排序问题的孤立值。例如,用于询问线程停止的bool shouldHalt。在我的经验中,这种隔离在正常软件开发中并不罕见。我认为,在另一种情况下(多个变量之间的依赖关系),锁几乎总是更好的选择。我唯一看到人们可能会遇到困难的方式是使用花哨的原子链式数据结构,除非有严重的需求,否则这才是真正需要注意的地方。 - VoidStar
在《C++并发编程实战(第二版)》中,在介绍5.2.1节的std::memory_order之后,您使用了许多不同的排序方式(甚至在解释5.3.3节中它们实际上是做什么之前),而不是像您在这个答案中建议的那样坚持使用std::memory_order_seq_cst。这是否意味着这个答案(或书籍)是错误的,还是我误解了什么? - user11313931
书中第5.2节的示例指定了内存顺序,以展示如何进行选择。对于真实的代码,除非您确实知道自己在做什么并且它是已证明的性能瓶颈,否则应坚持使用std::memory_order_seq_cst - Anthony Williams

29
有人能用简单的英语解释一下 std::memory_order 是什么吗?最好不要使用解释性质的内容。以下是 Bartoz Milewski 对松弛原子操作的最佳解释:http://bartoszmilewski.com/2008/12/01/c-atomics-and-memory-ordering/ 和这篇文章的后续内容:http://bartoszmilewski.com/2008/12/23/the-inscrutable-c-memory-model/。但请注意,虽然这些文章是很好的介绍,但它们出版之前的 C++11 标准已经过时了,不能告诉您如何安全地使用它们。
关于如何在 std::atomic<> 中使用它们,我的最佳建议是:最好不要。松弛原子可能是 C++11 中最麻烦和最危险的东西。使用默认内存顺序(顺序一致性)的 std::atomic<T> 直到您确信存在可通过使用松弛内存顺序解决的性能问题为止。
在上面链接的第二篇文章中,Bartoz Milewski 得出了以下结论:
“当我尝试理解 C++ 弱原子时,我完全不知道自己在做什么。它背后的理论是如此复杂,以至于几乎无法使用。我们需要三个人(Anthony、Hans 和我)以及对标准的修改才能完成一个相对简单算法的证明。想象一下为基于弱原子操作的无锁队列做同样的事情!”

谢谢您的建议。作为一个初学者,我觉得这非常有帮助。 - 2607
1
请查看Jeff Preshing的文章,比如这篇关于获取/释放语义的文章,其中有很好的入门材料,不仅描述了C++11,还用允许/禁止的内存重排序类型来描述事物。(例如,StoreLoad重排序是允许的) - Peter Cordes
3
“relaxed”更容易理解:当你只需要原子性而不需要同步时,请使用它。例如,对于多个线程将增加的单个原子计数器,但这对于任何其他数据的有效性没有意义。 - Peter Cordes

18

编号。一个“简明易懂”的解释需要32页,可以在此处找到。

如果您不想阅读该页面,则可以忘记内存排序,因为您链接的页面表明默认是顺序一致性排序,这是“始终执行合理操作”的设置。

要使用任何其他设置,您必须真正阅读并理解上述文章及其中的示例。


5
简而言之,编译器和CPU可能会按照与您编写顺序不同的顺序执行指令。对于单线程来说,这不是问题,因为它看起来是正确的。但对于多个处理器上的多个线程来说,这就成为了一个问题。C++中的内存排序限制了编译器/CPU可以做的事情,并解决了这些问题。
例如,如果您查看我的双重检查锁定文章,您可以看到如何使用原子内存排序来解决排序混乱的问题。
关于重新排序本身,您还可以考虑CPU重新排序--同样,编译器也可能正在进行重新排序。
请注意,任何关于此主题的文档(包括我的)都提供理论场景。像x86这样的最常见的CPU具有非常强的排序保证,因此很多明确的排序实际上是不需要的。因此,即使您不使用适当的C++11原子,您的代码也可能仍然有效。
正如zvrba所提到的,这个主题实际上非常详细。内存屏障的Linux内核文档也包含了很多详细的信息。

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接