我不是专家,这个问题很复杂,请随意评论我的帖子。正如mdh.heydari指出的那样,cppreference.com比Rust(C++具有几乎相同的API)提供了更好的排序文档。
对于你的问题
你需要在生产者中使用“release”排序,在消费者中使用“acquire”排序。这可以确保数据变异发生在 AtomicBool
设置为 true 之前。
如果你的队列是异步的,那么消费者将需要在循环中不断尝试从中读取,因为生产者在设置 AtomicBool
并将其放入队列之间可能会被打断。
如果生产者代码可能在客户端运行之前运行多次,则不能使用 RefCell
,因为它们可能在客户端读取数据时对其进行变异。否则没问题。
还有其他更好、更简单的方法来实现这个模式,但我假设你只是举了一个例子。
什么是排序?
不同的排序与另一个线程在原子操作发生时看到发生了什么有关。编译器和CPU通常都允许重新排序指令以优化代码,而排序影响它们允许重新排序指令的程度。
您可以始终使用SeqCst
,这基本上保证每个人都会看到相对于其他指令放置的指令作为已发生,但在某些情况下,如果指定较少限制性的排序,则LLVM和CPU可以更好地优化您的代码。
您应该将这些排序视为适用于内存位置(而不是适用于指令)。
排序类型
松散顺序
除了修改内存位置是原子的(因此它完全发生或根本不发生)之外,没有任何限制。如果个别线程检索/设置的值不重要,只要它们是原子性的,那么这对于像计数器这样的东西就可以接受。
获取顺序
这个约束规定了在“acquire”应用后,发生在代码中的任何变量读取都不能被重新排序到它之前。例如,在您的代码中,您读取了一些共享内存位置并获得了值“X”,该值在时间“T”存储在该内存位置中,然后应用了“acquire”约束。在应用约束之后从中读取的任何内存位置将具有时间“T”或更晚的值。
这可能是大多数人直觉上期望发生的事情,但由于CPU和优化器可以重新排序指令,只要它们不改变结果,这并不是保证的。
为了使“acquire”有用,它必须与“release”配对,否则无法保证另一个线程没有将其写入指令重新排序到更早的时间。
通过使用Acquire-读取您正在查找的标志值,这意味着您不会在释放存储到标志之前看到实际上由写入更改的过时值。
Release Ordering
这个限制规定了在应用“release”之前发生的任何变量写操作都不能被重新排序为在其之后发生。因此,在代码中写入一些共享内存位置,然后在时间
T
设置某些内存位置t,然后应用“release”约束条件。在应用“release”之前出现在您的代码中的任何写入都保证已经在它之前发生。
同样,这是大多数人直觉上期望发生的事情,但如果没有约束条件,这并不是保证的。
如果尝试读取值
X
的另一个线程没有使用“acquire”,那么它不能保证在其他变量值更改方面看到新值。因此,它可能会获取新值,但可能无法看到任何其他共享变量的新值。还要记住测试很难。一些硬件在实践中不会显示某些不安全代码的重新排序,因此问题可能无法被检测到。
Jeff Preshing写了一个关于获取和释放语义的很好的解释,如果不清楚,请阅读该解释。
AcqRel排序
这个同时使用了 Acquire
和 Release
排序(即两种限制都适用)。我不确定什么时候需要使用它——如果有三个或更多线程参与的情况下,一些线程使用 Release
,一些使用 Acquire
,还有一些同时使用两种,可能会有帮助,但我并不确定。
SeqCst 排序
这是最严格也是最慢的选项。它强制内存访问在每个线程中以相同的顺序出现。这要求在x86上对原子变量的所有写操作都使用
MFENCE
指令(包括StoreLoad的完整内存屏障),而较弱的排序则不需要。(在
this C++ compiler output中可以看到,SeqCst加载在x86上不需要屏障。)
读取-修改-写入访问,例如原子递增或比较和交换,在x86上使用锁定指令完成,这些指令已经是完整的内存屏障。如果您关心在非x86目标上编译有效代码,那么尽可能避免使用SeqCst,即使对于原子读取-修改-写入操作也是如此。但是,有些情况下需要。
更多关于原子语义如何转换成ASM的例子,请参见
这个包含简单函数的C++原子变量集合。我知道这是一个Rust问题,但它应该基本上具有与C++相同的API。godbolt可以针对x86、ARM、ARM64和PowerPC进行目标代码生成。有趣的是,ARM64具有加载-获取(
ldar
)和存储-释放(
stlr
)指令,因此它并不总是需要使用单独的屏障指令。
顺便说一下,x86 CPU 默认始终是“强排序”的,这意味着它们始终像设置了至少 AcqRel
模式一样工作。因此,对于 x86,“排序”仅影响 LLVM 优化器的行为。另一方面,ARM 是弱排序的。默认情况下设置为Relaxed
,以允许编译器完全自由地重新排序,而不需要在弱排序的 CPU 上要求额外的屏障指令。
SeqCst
),因为它是最严格的选项(我最不可能搞砸),而且我不知道其他选项的含义。^_^ - ShepmasterMFENCE
指令,而所有较弱的排序都不会(因为x86会在每次加载和存储时免费执行它们)。Jeff Preshing的博客有一些很棒的材料,可以帮助您理解内存排序,例如http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/。我最近写了一个答案,其中包含了许多链接到该网站和其他来源的链接:https://dev59.com/WVwY5IYBdhLWcg3wpJE-#32394427 - Peter Cordes