在这种情况下,原子读取操作memory_order_seq_cst读取哪个值

Which value does atomic read operation with memory_order_seq_cst read in this situation?

本文关键字:读取 order seq cst memory 这种情况下 操作      更新时间:2023-10-16

我已经阅读了 c++11 标准中有关内存排序的章节,并对规则感到困惑。根据C++11标准(ISO/IEC JTC1 SC22 WG21 N3690),29.3 3,据说:

所有memory_order_seq_cst操作的总顺序 S 应与所有受影响位置的"发生之前"顺序和修改顺序一致,因此从原子对象 M 加载值的每个memory_order_seq_cst操作 B 都观察到以下值之一:
— 最后一次修改 A 的结果 M 在 S 中 B 之前, 如果它存在,或者
——如果A存在,那么在对B的可见副作用序列中M进行某种修改的结果,这种修改不是memory_order_seq_cst的,也不会在A之前发生,或者
——如果A不存在,那么M在对B的可见副作用序列中对M进行某种修改的结果,这是不memory_order_seq_cst的。

因此,请考虑以下情况:

有 4 个原子操作ABCD。 从代码:

  • 所有这些都是对同一原子变量的操作
  • AB是任何顺序的写入操作(可以放宽)
  • C是带有memory_order_seq_cst的写入操作
  • D是带memory_order_seq_cst的读取操作
  • AD之前发生的最后一个写入操作
  • A,B,C没有相互发生之前的关系。
  • D,B,C没有相互发生之前的关系。

考虑恰好发生以下排序的执行:

  • C出现在D之前,用于memory_order_seq_cst操作的单个总订单
  • 此变量的修改顺序类似于A->B->C

这是可能的代码

using namespace std;
atomic_bool go(false);
atomic_int var(0);
void thread1()
{
while (!go) {}
var.store(1, memory_order_relaxed);              // A
this_thread::yield();
cout << var.load(memory_order_seq_cst) << endl;  // D
}
void thread2()
{
while (!go) {}
var.store(2, memory_order_seq_cst);              // C
}
void thread3()
{
while (!go) {}
var.store(3, memory_order_relaxed);              // B
}
int main() {
thread t1(thread1);
thread t2(thread2);
thread t3(thread3);
go = true;
t1.join();
t2.join();
t3.join();
}

读取操作D是否有可能读取操作 B 写入的值,给定 A、B、C 的修改顺序var

如果不可能,哪些规则排除了这种可能性?

如果可能,这意味着memory_order_seq_cst可以读取"在最后一次写入之前"写入"的值memory_order_seq_cst。这是C++标准的"错误",还是在并非一切都seq_cst时故意设计的?

在这种情况下,D 可能会从 A、B 或 C 读取。

考虑一个包含四个节点的图形:A、B、C 和 D。 和边(sc:顺序一致(总)排序(

C --sc--> D),sb:在之前/之前发生排序(A --sb--> D),mo:修改顺序(A --mo--> B --mo--> C)和rf:读取自(? --rf--> D))。图中的射频边与C++记忆模型不一致有两个原因:因果关系和因为您无法从隐藏的视觉副作用中读取。

如果你暂时忽略sc边缘,那么 - 只有一个原子变量,图上唯一的因果限制是没有涉及rf边和(定向)sb边的循环(这是我研究的结果)。在这种情况下,甚至不存在这样的循环,因为你只有一个射频边沿 - 所以,没有任何理由你不能从三个写入中的任何一个读取。

但是,您同时指定确切的修改顺序(恕我直言,这并不重要,您应该只对程序的可能结果感兴趣)以及一个 sc 边缘。我们仍然需要研究这些是否与三种可能的射频边缘中的每一个兼容,以便从隐藏的视觉副作用中读取。

请注意,如果给定的 rf 边沿的写入节点被释放并且读取节点被获取,则会引入同步;sc 是释放/获取的,因此后者为真,前者仅在从节点 C 读取时为真。但是,同步意味着永远不会超过(按修改顺序)写入之前的所有内容都必须在读取之后的所有内容之前发生;读取后什么都没有,所以整个同步无关紧要。

此外,听写修改顺序(A --mo--> B --mo--> C)与指示的总sc顺序(C --sc--> D)没有因果关系,因为D是读取而不是修改顺序子图的一部分。唯一不允许的(由于因果关系)是涉及 sc 和 mo 边缘的有向循环。

现在,作为一个实验,假设我们也使节点 A 成为 sc。然后我们需要将 A 放入总排序中,因此 A --sc--> C --sc--> D,C --sc--> A --sc--> D 或 C --sc--> D --sc--> A,但我们有 A --mo--> C,因此后两者是不允许的(会导致(因果)循环),唯一可能的排序是:A --sc--> C --sc--> D。现在不再可能从 A 读取,因为这会导致以下子图:

A --sc--> C
|        /
|       /
|      /
rf    sc
|    /
|   /
|  /
v v
D

并且 C 中的写入将始终覆盖 A 在被 D 读取之前写入的值(又名 A 是 D 隐藏的视觉副作用)。

如果 A 不是 sc(就像原始问题中的情况一样),那么只有在以下情况下才不允许此 rf (因为隐藏的 vse)

A --hb--> C
|        /
|       /
|      /
rf    sc
|    /
|   /
|  /
v v
D

其中'hb'代表Happen Before(出于同样的原因;那么A是D隐藏的视觉副作用,因为C总是在D读取之前覆盖A写入的值)。

然而,在原始问题中,线程 1 和 2 之间没有先行发生,因为这种同步需要在两个线程之间(或栅栏或任何会导致额外同步的东西)之间出现另一个 rf 边。

最后,是的,这是预期行为,而不是标准中的错误。

编辑

引用您引用的标准:

— M 的最后一次修改的结果,在 S 中 B 之前,如果 它存在,或

这里的A是你的C,B是你的D。这里提到的 A 确实存在,即节点 C (C --sc--> D)。所以这一行说可以读取节点 C 写入的值。

— 如果 A 存在,则在可见光中对 M 进行某种修改的结果 关于B的副作用序列不是memory_order_seq_cst,这不会发生在 A 之前,或者

同样,这里的A是你的C,它存在。那么"在可见的副作用序列中对M(var)的一些修改的结果,相对于B(你的D)没有memory_order_seq_cst">是你的A。正如我们已经确定的那样,您的 A 不会先于您的 C(他们的 A)发生。因此,这表示可以从您的 A 读取写入的值。

— 如果 A 不存在,则 M 在 相对于B的可见副作用序列不是memory_order_seq_cst.

这在这里无关紧要,并且仅适用于在 B(您的 D)之前的总排序 M 的 S (var) 中没有写入的情况。

即使将A->B->C作为"修改顺序">B也是一种可能性,因为它"不memory_order_seq_cst,并且在[C]之前不会发生"。

可见副作用序列(1.10.14)的标准定义支持这一点(强调我的):

关于 M 的值计算 B,对原子对象 M 的可见副作用序列是M修改顺序中副作用的最大连续子序列,其中第一个副作用相对于B是可见的,并且对于每个副作用,B 不是发生在它之前的情况。由评估 B 确定的原子对象 M 的值应是M相对于B的可见序列中的某个操作存储的值。

因此,即使有明确的修改顺序,您的负载也可以产生ABC