Skip to content

Commit

Permalink
Update 3_Memory_Ordering.md about #95 (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
fwqaaq authored Apr 24, 2024
1 parent cae3e96 commit ad8a4d3
Showing 1 changed file with 10 additions and 6 deletions.
16 changes: 10 additions & 6 deletions 3_Memory_Ordering.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,11 @@ consume 排序是 acquire 排序的一个轻量级、更高效的变体,其同

最强的内存排序是*顺序一致性*排序:`Ordering::SeqCst`。它包含了 acquire 排序(对于 load 操作)以及 release 排序(对于 store 操作)的所有保证,并且**保证了操作的全局一致排序。

这意味着在程序中使用 `SeqCst` 排序的每个操作是所有线程都同意的单个总顺序的一部分。这个总顺序与每个变量的总修改顺序一致
这意味着在程序中使用 `SeqCst` 排序的每个操作是所有线程都同意的单独全序[^3](single total order,译注:可以理解为在该顺序关系中,每个操作都与其它操作有单独的顺序关系)的一部分。该全序[^4](total order,译注:该原子变量的顺序关系)与每个单独变量的总修改顺序(total modification order)一致

由于它严格强于 acquire 和 release 内存排序,因此顺序一致性 load 或者 store 操作可以取代一对 release-acquire 中的 acquire-load 或 release-store 操作,形成 happens-before 关系。换句话说,acquire-load 不仅可以与 release-store 形成 happens-before 关系,同时也可以和顺序一致的 store 形成 happens-before 关系,同样 release-store 也是如此。

> 仅有当 happens-before 关系的双方都使用 SeqCst 时,才能保证与 SeqCst 操作的单个总顺序一致
> 仅有当 happens-before 关系的双方都使用 SeqCst 时,才能保证与 SeqCst 操作的单独全序一致
虽然这似乎是最容易推理的内存排序,但 SeqCst 排序在实践中几乎从来都没有必要。在几乎所有情况下,通常 acquire 和 release 排序就足够了。

Expand Down Expand Up @@ -499,7 +499,7 @@ fn main() {

两个线程首先设置它们自己的原子布尔值到 true,以告知另一个线程它们正在获取 S,并且然后检查其它的原子变量布尔值,是否它们可以安全地获取 S,而不会导致数据竞争。

如果两个 store 操作都发生在任一 load 操作之前,则两个线程最终都无法访问 S。然而,两个线程都不可能访问 S 并导致未定义的行为,因为顺序一致的顺序保证了其中只有一个线程可以赢得竞争。在每个可能的单个总的顺序中,第一个操作将是 store 操作,这阻止其他线程访问 S。
如果两个 store 操作都发生在任一 load 操作之前,则两个线程最终都无法访问 S。然而,两个线程都不可能访问 S 并导致未定义的行为,因为顺序一致的顺序保证了其中只有一个线程可以赢得竞争。在每个可能的单独全序中,第一个操作将是 store 操作,这阻止其他线程访问 S。

在实际情况中,几乎所有对 SeqCst 的使用都涉及一种类似的存储模式, store 操作在随后同一线程上的 load 操作之前必须成为全局可见的。对于这些情况,一个潜在的更有效的替代方案是将 relaxed 的操作与 SeqCst 屏障结合使用,我们接下来将探索。

Expand All @@ -509,7 +509,7 @@ fn main() {

除了对原子变量的额外操作,我们还可以将内存排序应用于:原子屏障。

`std::sync::atomic::fence` 函数表示一个*原子屏障*,它可以是一个 Release 屏障、一个 Acquire 屏障,或者两者都是(AcqRel 或 SeqCst)。SeqCst 屏障还参与顺序一致性的全局排序
`std::sync::atomic::fence` 函数表示一个*原子屏障*,它可以是一个 Release 屏障、一个 Acquire 屏障,或者两者都是(AcqRel 或 SeqCst)。SeqCst 屏障还参与到顺序一致性的全序中

原子屏障允许你从原子操作中分离内存排序。如果你想要应用内存排序到多个操作这可能是有用的,或者你想要有条件地应用内存排序。

Expand Down Expand Up @@ -634,7 +634,7 @@ fn main() {

虽然在这个特定的例子中,投入任何精力进行此类优化可能完全没有必要,但在构建高效的并发数据结构时,这种节省额外获取操作开销的模式可能很重要。

`SeqCst` 屏障既是 release 屏障也是 acquire 屏障(就像 `AcqRel`),同时也是顺序一致性操作的单个总顺序的一部分。然而,只有屏障是总顺序的一部分,但它之前或之后的原子操作不必是总顺序的一部分。这意味着,与 release 或 acquire 操作不同,顺序一致性的操作不能拆分为 relaxed 操作和内存屏障。
`SeqCst` 屏障既是 release 屏障也是 acquire 屏障(就像 `AcqRel`),同时也是顺序一致性操作中单独全序的一部分。然而,只有屏障是全序的一部分,但它之前或之后的原子操作不必是总顺序的一部分。这意味着,与 release 或 acquire 操作不同,顺序一致性的操作不能拆分为 relaxed 操作和内存屏障。

<div class="box">
<h2 style="text-align: center;">编译器屏障</h2>
Expand Down Expand Up @@ -685,7 +685,7 @@ fn main() {
抛开性能问题,顺序一致性内存排序通常被视为默认选择的理想内存排序类型,因为它具有强大的保证。确实,如果任何其他内存排序是正确的,那么 `SeqCst` 也是正确的。这可能让人觉得 `SeqCst` 总是正确的。然而,可能并发算法本身就是不正确的,不论使用哪种内存排序。

更重要的是,在阅读代码时,`SeqCst` 基本上告诉读者:“该操作依赖于程序中每个 `SeqCst` 操作的总顺序”,这是一个极其广泛的声明。如果可能的话,使用较弱的内存排序往往会使相同的代码更容易进行审查和验证。例如,Release 明确告诉读者:“这与同一变量的 acquire 操作相关”,在形成对代码的理解时涉及的考虑要少得多。
更重要的是,在阅读代码时,`SeqCst` 基本上告诉读者:“该操作依赖于程序中每个单独 `SeqCst` 操作的全序”,这是一个极其广泛的声明。如果可能的话,使用较弱的内存排序往往会使相同的代码更容易进行审查和验证。例如,Release 明确告诉读者:“这与同一变量的 acquire 操作相关”,在形成对代码的理解时涉及的考虑要少得多。

建议将 SeqCst 看作是一个警示标识。在实际代码中看到它通常意味着要么涉及到复杂的情况,要么简单地说是作者没有花时间分析其与内存排序相关的假设,这两种情况都需要额外的审查。

Expand Down Expand Up @@ -717,3 +717,7 @@ fn main() {

[^1]: <https://zh.wikipedia.org/wiki/内存排序>
[^2]: <https://zh.wikipedia.org/wiki/内存屏障>
[^3]: 摘自 CPP 原子变量的内存排序翻译,请参考参见
[^4]: 摘自 CPP 原子变量的内存排序翻译,请参考参见

参见:<https://zh.cppreference.com/w/cpp/atomic/memory_order>

0 comments on commit ad8a4d3

Please sign in to comment.