在这种情况下,读取memory_order_seq_cst的原子读操作值是多少?

时间:2017-06-09 02:49:59

标签: c++ c++11 concurrency atomic memory-barriers

我已经阅读了关于c ++ 11标准中的内存排序的章节,并且被规则搞糊涂了。根据C ++ 11标准(ISO / IEC JTC1 SC22 WG21 N3690),29.3 3,它说:

  

所有memory_order_seq_cst操作上应该有一个总订单S,与所有受影响位置的“发生之前”订单和修改订单一致,这样每个从原子对象M加载值的memory_order_seq_cst操作B都会观察到其中一个以下值:
- S中B的最后修改A的结果,如果存在,或者 - 如果A存在,则在可见序列中对M进行一些修改的结果关于B的副作用不是memory_order_seq_cst而是在A之前没有发生,或者是 - 如果A不存在,则对于B的副作用的可见序列中的M的一些修改的结果是不是memory_order_seq_cst。

因此,请考虑以下情况:

有4个原子操作 A B C D

  • 所有这些都是对同一原子变量的操作
  • A B 是任何订单的写入操作(可放宽)
  • C 是使用memory_order_seq_cst
  • 的写入操作
  • D 是使用memory_order_seq_cst
  • 的读取操作
  • 发生之前的最后一次写入操作 D
  • A B C 之前没有发生过关系。
  • D B C 之间没有发生过关系。
  • C 出现在memory_order_seq_cst操作的单个总订单中的 D 之前
  • 此变量的修改顺序显示为 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 写入的值?如果不可能,有哪些规则排除这种可能性?如果可能,这意味着memory_order_seq_cst可以读取最后一次memory_order_seq_cst写入“之前写入”的值。这是c ++标准中的“bug”,还是故意设计的?

2 个答案:

答案 0 :(得分:1)

即使 A -> B -> C 作为“修改订单” B 是有可能的,因为它“不是memory_order_seq_cst,并且不会在[ C ]” 之前发生。

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

  

相对于 M 的值计算 B ,原子对象 M 可见的副作用序列 em>是按 M 的修改顺序排列的副作用的最大连续子序列,其中第一个副作用对于 B 可见,并且对于每个侧面实际上,不是 B 发生在它之前。由评估 B 确定的原子对象 M 的值应为 some 操作在可见序列中存储的值。 > M 关于 B

因此,即使有明确的修改订单,您的负载也可以产生 A B C

答案 1 :(得分:0)

在这种情况下,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)读取。

图表中的rf边缘与C ++内存模型不一致有两个原因:因果关系,因为你无法从隐藏的视觉副作用中读取。

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

但是,你指定两者,确切的修改顺序(不是这对imho来说很重要,你应该只对程序的可能结果感兴趣),以及一个sc边缘。我们仍然需要调查这些是否与三个可能的rf边缘中的每一个相兼容,以便从隐藏的视觉副作用中读取。

注意,如果给定的rf边缘的写节点被释放并且读取节点被获取,则引入同步; sc是释放/获取,所以后者是真的而前者只有在从节点C读取时才是真的。然而,同步意味着永远不会超过(按修改顺序)写入之前的所有内容必须在读取之后的所有内容之前发生;阅读后没有任何内容,因此整个同步并不重要。

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

现在,作为一个实验,假设我们也将节点A设为sc。然后我们需要在总排序中加上A,因此A - sc - > C --sc - &gt; D,C - sc - &gt; A --sc - &gt; D或C --sc - &gt; D --sc - &gt; A,但我们有A - 莫 - > C,因此后两者是不允许的(会导致(因果)循环)并且唯一可能的排序是:A - sc - &gt; C --sc - &gt; 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

其中&#39; hb&#39;代表Happens-Before(出于同样的原因;然后A是D的隐藏视觉副作用,因为在D读取之前C将始终覆盖A写入的值)。

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

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

修改

引用您引用的标准:

— the result of the last modification A of M that precedes B in S, if it exists, or

这里的A是你的C,这里的B是你的D.这里提到的A存在,即节点C(C -sc-gt; D)。所以这一行表示可以读取节点C写的值。

— if A exists, the result of some modification of M in the visible sequence of side effects with respect to B that is not memory_order_seq_cst and that does not happen before A, or

同样,这里的A是你的C而且它存在。然后,对于B(你的D)的副作用的可见序列中的M(var)的一些修改的结果是 not memory_order_seq_cst&#34;是你的A.而且正如我们已经建立的那样,你的A不会发生在你的C(他们的A)之前。因此,这表示可以读取从A中写入的值。

— if A does not exist, the result of some modification of M in the visible sequence of side effects with respect to B that is not memory_order_seq_cst.

这与此处无关,只有在B(您的D)之前的M(var)的总排序S中没有写入时才适用。