13

cppreference.com上的文档中std::memory_order有一个轻松排序的示例:

轻松订购

标记memory_order_relaxed的原子操作不是同步操作;它们不会在并发内存访问之间强加顺序。它们只保证原子性和修改顺序的一致性。

例如,x 和 y 最初为零,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

允许产生 r1 == r2 == 42,因为尽管 A在线程 1中排在B 之前,C 在线程 2 中排在 D之前,但没有什么可以阻止 D 在 y 的修改顺序中出现在 A 之前,而 B 从在 x 的修改顺序中出现在 C 之前。D 对 y 的副作用可能对线程 1 中的负载 A 可见,而 B 对 x 的副作用可能对线程 2 中的负载 C 可见。特别是,如果 D 在 C 中的 C 之前完成,则可能会发生这种情况线程 2,由于编译器重新排序或在运行时。

它说“C 在线程 2 中在 D 之前排序”。

根据排序前的定义,可以在评估顺序中找到,如果 A 在 B 之前排序,则 A 的评估将在 B 的评估开始之前完成。由于线程 2 中 C 在 D 之前排序,因此 C 必须在 D 开始之前完成,因此快照最后一句的条件部分将永远不会满足。

4

3 回答 3

13

我相信 cppreference 是正确的。我认为这归结为“as-if”规则[intro.execution]/1。编译器只能重现您的代码所描述的程序的可观察行为。从执行这些评估的线程的角度来看,仅在评估之间建立先序关系[intro.execution]/15。这意味着当在某个线程的某个地方出现一个接一个的两个评估时,该线程中实际运行的代码必须表现得好像第一个评估确实影响了第二个评估所做的任何事情。例如

int x = 0;
x = 42;
std::cout << x;

必须打印 42。但是,编译器实际上不必将值 42 存储到对象x中,然后再从该对象读取值以打印它。它可能还记得要存储的最后一个值x是 42 ,然后在将值 42 实际存储到 之前直接打印值 42 x。事实上,如果x是一个局部变量,它也可以只跟踪该变量在任何时候最后分配的值,甚至从不创建对象或实际存储值 42。线程无法区分。行为总是好像有一个变量,并且好像值 42 之前实际上存储在一个对象x 从该对象加载。但这并不意味着生成的机器代码必须在任何地方实际存储和加载任何内容。所需要的只是生成的机器代码的可观察行为与所有这些事情都实际发生时的行为是无法区分的。

如果我们看

r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

那么是的,C 在 D 之前排序。但是从这个线程单独来看,C 所做的任何事情都不会影响 D 的结果。D 所做的任何事情都不会改变 C 的结果。唯一可能影响另一个的方法是作为另一个线程中发生的事情的间接后果。但是,通过指定std::memory_order_relaxed,您明确指出另一个线程观察加载和存储的顺序无关紧要。由于没有其他线程可以以任何特定顺序观察加载和存储,因此没有其他线程可以做任何事情来使 C 和 D 以一致的方式相互影响。因此,实际执行加载和存储的顺序是无关紧要的。因此,编译器可以自由地重新排序它们。而且,正如该示例下方的解释中所提到的,如果从 D 中的存储在从 C 中加载之前执行,那么 r1 == r2 == 42 确实可以发生......

于 2020-01-11T17:56:00.157 回答
1

有时可以相对于其他两个动作序列对动作进行排序,而不暗示这些序列中的动作相对于彼此的任何相对排序。

例如,假设有以下三个事件:

  • 将 1 存储到 p1
  • 将 p2 加载到 temp
  • 将 2 存储到 p3

并且 p2 的读取在 p1 写入之后和 p3 写入之前独立排序,但是 p1 和 p3 没有特定的顺序参与。根据对 p2 所做的事情,编译器将 p1 推迟到 p3 之后仍然使用 p2 实现所需的语义可能是不切实际的。然而,假设编译器知道上面的代码是一个更大序列的一部分:

  • 将 1 存储到 p2 [在 p2 加载之前排序]
  • [执行上述操作]
  • 将 3 存储到 p1 [在另一个存储到 p1 之后排序]

在这种情况下,它可以确定它可以在上述代码之后将存储重新排序到 p1 并将其与以下存储合并,从而导致写入 p3 而不先写入 p1 的代码:

  • 将温度设置为 1
  • 将温度存储到 p2
  • 将 2 存储到 p3
  • 将 3 存储到 p1

尽管看起来数据依赖性会导致排序关系的某些部分表现出传递性,但编译器可能会识别出明显的数据依赖性不存在的情况,因此不会产生预期的传递性影响。

于 2020-01-11T18:16:23.450 回答
1

如果有两个语句,编译器将按顺序生成代码,因此第一个语句的代码将放在第二个语句之前。但是 cpus 内部有管道并且能够并行运行汇编操作。语句 C 是加载指令。在获取内存时,流水线将处理接下来的几条指令,并且鉴于它们不依赖于加载指令,它们最终可能在 C 完成之前被执行(例如,D 的数据在缓存中,C 在主存储器中)。

如果用户确实需要两个语句顺序执行,可以使用更严格的内存排序操作。一般来说,只要程序在逻辑上是正确的,用户就不会关心。

于 2020-01-11T19:42:03.083 回答