cppreference中对宽松顺序的解释是否错误?


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,没有什么阻止由在y的修改次序出现A之前D和B,从按x的修改顺序出现在C之前。D在y上的副作用对于线程1中的负载A可见,而B在x上的副作用对于线程2中的负载C可见。特别是,如果D在C in之前完成,则可能发生这种情况。线程2,由于编译器重新排序或在运行时。

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

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


您的问题专门关于C ++ 11吗?
curiousguy

不,它也适用于c ++ 14,17。我知道编译器和CPU都可以用D对C进行重新排序。但是,如果发生重新排序,则C不能在D开始之前完成。因此,我认为句子“ A在线程1内在B之前排序,而C在线程2内在D之前排序C”中存在术语滥用。准确地说:“在代码中,线程1中A位于B之前,而线程2中C位于D之前。” 这个问题的目的是为了确认这一思想
abigaile

“重新排序”一词没有定义。
curiousguy

Answers:


12

我相信cppreference是正确的。我认为这可以归结为“假设”规则[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的确可以实现……


因此,从本质上说,标准规定C必须在D之前发生,但是编译器认为无法证明C还是D在接下来发生,并且由于视情况规则而对它们进行了重新排序,对吗?
Fureeish

4
@Fureeish No. C必须在D之前发生,直到它们发生的线程可以知道的为止。从另一个上下文观察可能与该观点不一致。
重复数据删除器

没有“好像规则”
curiousguy

4
@curiousguy此主张似乎与您先前的其他C ++福音派相似。
在轨道进行的轻度比赛

1
@curiousguy该标准的确在脚注标记了其中一项规定“即用规则”:intro.execution
Caleth

1

有时,一个动作可能相对于两个其他动作序列进行排序,而并不意味着这些序列中的动作相对于彼此具有任何相对顺序。

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

  • 存储1至p1
  • 将p2加载到温度
  • 存储2至p3

并且p2的读取在p1写入之后和p3写入之前是独立排序的,但是p1和p3都没有参与的特定排序。根据对p2所做的操作,对于编译器而言,将p1推迟到p3之后并仍然通过p2获得所需的语义可能是不切实际的。但是,假设编译器知道上述代码是较大序列的一部分:

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

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

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

尽管似乎数据依赖性将导致顺序关系的某些部分具有传递性,但是编译器可能会确定不存在明显的数据依赖性的情况,因此不会产生预期的传递性影响。


1

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

如果用户确实需要按顺序执行这两个语句,则可以使用更严格的内存排序操作。通常,只要程序在逻辑上正确,用户就不会在意。


-7

无论您认为什么都同样有效。该标准没有说明顺序执行什么,什么不执行以及如何混合

由您和每个程序员决定,在那堆混乱的基础上构造出一致的语义,这项工作值得多名博士。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.