应该使用哪个std::sync::atomic::Ordering?

58
所有的 std::sync::atomic::AtomicBool 方法都采用了内存顺序(Relaxed、Release、Acquire、AcqRel 和 SeqCst),这是我以前没有使用过的。 在什么情况下应该使用这些值?文档使用了混淆的“load”和“store”术语,我真的不明白。例如:

生产者线程改变一个由 Mutex 持有的状态,然后调用 AtomicBool::compare_and_swap(false, true, ordering)(以合并失效),如果交换成功,则将“invalidate”消息发布到并发队列(例如 mpsc 或 winapi 的 PostMessage)。消费者线程重置 AtomicBool,从队列中读取数据,并读取由 Mutex 保持的状态。生产者可以使用 Relaxed 顺序吗,因为它在互斥锁之前,还是必须使用 Release?消费者可以使用 store(false, Relaxed),还是必须使用 compare_and_swap(true, false, Acquire) 从互斥锁接收更改?

如果生产者和消费者共享 RefCell 而不是 Mutex 会怎样?


6
内存顺序与LLVM(或C ++;Rust文档不一致)相同,因此您可能会发现这些链接有用:这个这个这个 - Jorge Israel Peña
8
就我个人而言,我使用“顺序一致性”(SeqCst),因为它是最严格的选项(我最不可能搞砸),而且我不知道其他选项的含义。^_^ - Shepmaster
1
请点击此处阅读:http://en.cppreference.com/w/cpp/atomic/memory_order - mdh.heydari
1
@Shepmaster:在C++中,seq_cst通常会在x86上产生一个MFENCE指令,而所有较弱的排序都不会(因为x86会在每次加载和存储时免费执行它们)。Jeff Preshing的博客有一些很棒的材料,可以帮助您理解内存排序,例如http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/。我最近写了一个答案,其中包含了许多链接到该网站和其他来源的链接:https://dev59.com/WVwY5IYBdhLWcg3wpJE-#32394427 - Peter Cordes
Herb Sutter在C++中有一个关于原子操作的精彩演讲:https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2。我认为这些内容基本上都适用于Rust。演讲的总结是,SeqCst是你大多数时候想要的。 - Jack O'Connor
文档使用令人困惑的“load”和“store”术语,我并不真正理解。我认为理解这些术语是使用原子操作的先决条件。因此,请确保阅读其他评论和答案中提供的优秀链接。 - Sebastian Redl
1个回答

38

我不是专家,这个问题很复杂,请随意评论我的帖子。正如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排序

这个同时使用了 AcquireRelease 排序(即两种限制都适用)。我不确定什么时候需要使用它——如果有三个或更多线程参与的情况下,一些线程使用 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 上要求额外的屏障指令。


3
我在 http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ 找到了一些很好的文章,有助于我理解这方面的内容。SeqCst 要求 CPU 防止 StoreLoad 重排序,这需要在 x86 上使用 MFENCE 指令,这是很昂贵的。这不是“锁定总线”的问题,只是无法使用在最后一个存储变为全局可见之前读取的预取数据。然而,在 x86 上,所有的读改写操作(例如原子增量或比较并交换)也都是完整的内存屏障,因此 SeqCst 与它们一起免费提供。 - Peter Cordes
我引入了一个错误,但我已经修复了:seq_cst对原子变量的加载不需要MFENCE,只需要存储。我链接了一些来自https://dev59.com/WVwY5IYBdhLWcg3wpJE-#32394427的C++编译器输出。如果Rust有类似的东西,那就太理想了,但我自己不懂Rust。我只是因为API与C++原子相同而跳进来的。 - Peter Cordes
1
@PeterCordes Godbolt有Rust版本,而Rust Playground同样允许输出ASM。如果可能的话,我建议添加Rust特定的输出。 - Shepmaster
@Shepmaster:感谢提供链接。我从未使用过Rust,所以我会把这个留给其他人(也许是Michael)。我同意Rust代码对于这个答案来说会更好。 - Peter Cordes

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