C++11中非原子变量的原子内存顺序

6

我不确定C++11中原子变量的内存排序保证如何影响对其他内存的操作。

假设我有一个线程定期调用写函数来更新值,另一个线程调用读函数以获取当前值。是否保证d = value;的效果在a = version;的效果之前不会被看到,并且在b = version;的效果之前被看到?

atomic<int> a {0};
atomic<int> b {0};
double d;

void write(int version, double value) {
    a = version;
    d = value;
    b = version;
}

double read() {
    int x,y;
    double ret;
    do {
        x = b;
        ret = d;
        y = a;
    } while (x != y);
    return ret;
}

你的代码中没有障碍,为什么要打上“内存屏障”的标签? - Kerrek SB
1
因为我认为答案可能是“这不正确,你需要使用内存屏障”。 - Jeffrey Dalla Tezza
1
你可能需要重新措辞你的问题。回答这个问题的人们过于拘泥小节,很可能会对那些对此感到好奇的人造成更多的伤害而不是帮助。 - Collin Dauphinee
我认为这是一个非常好的问题。在Christophe添加“然而”之后,他的答案完全没有意义。C ++内存模型的整个重点是指定“如何围绕原子操作对非原子内存访问进行排序。”(http://en.cppreference.com/w/cpp/atomic/memory_order),而他的答案完全忽略了这一点,并且被Collin Dauphinee所说的“令人作呕的学究气”。 - Kan Li
@CollinDauphinee 这种过于追求细节的说法听起来像是某种挫败的表达。你可以自由地提出一个不那么追求细节的建设性回答。 - Christophe
@icando 我承认一开始我错过了这个问题,因为我太专注于竞态条件了。很多人努力在线程之间共享非原子或易失性变量,我感觉自己像唐吉诃德一样。对此我深感抱歉。然而,从问题的标题来看,我认为值得提到的是,为什么只有在循环退出后d才是正确的。 - Christophe
3个回答

4
您的对象d被两个线程写入和读取,而且它不是原子的。这是不安全的,正如C++多线程标准中所建议的:
1.10/4:如果其中一个表达式修改了内存位置,并且另一个表达式访问或修改了相同的内存位置,则两个表达式评估冲突。
1.10/21:如果程序在不同线程中包含两个冲突操作(至少有一个操作不是原子的),并且没有一个操作发生在另一个之前,则程序的执行包含数据竞争。任何此类数据竞争都会导致未定义行为。
重要编辑:在非原子情况下,您无法保证读取和写入之间的顺序。您甚至不能保证读者将读取写入者写入的值(这篇短文解释了非原子变量的风险)。
然而,基于周围原子变量的测试,您的读取器循环将结束,并且存在强有力的保证。假设version在写入不同调用之间永远不会重复,并且给定您获取其值的相反顺序:
  • 如果两个原子相等,则d读取与d写入的顺序不能不幸。
  • 同样地,如果两个原子相等,则读取值不能不一致。
这意味着,在非原子性竞争的情况下,由于循环,您最终将读取最后一个value

@KerrekSB 对不起我的表述有些含糊。我想强调这是不安全的,并参考标准。我将其更改为“建议”。难道它不是未指定或未定义而不是损坏吗? - Christophe
@T.C. 确实。我想强调问题的原因并保持简短。您认为额外的细节,例如数据竞争的定义(pt.21)或副作用可见性规则,是否有助于OP? - Christophe
1
为什么这是未定义行为(这意味着任何事情都可能发生),而不是未指定的行为(读/写顺序未知)?注意:指的是内置类型。 - user2249683
1
@DieterLücking 因为适用于1.10/21。b是红色的并且同时编写。 - usr
2
@JeffreyDallaTezza 我做了一个重要的编辑:我太关注d上的竞争条件,没有注意到你是以相反的顺序读取a和b的 - 请继续阅读“然而”。 - Christophe
显示剩余7条评论

4
规则是,在执行一次的write线程给定,以及没有其他修改abd的东西,
  • 您可以随时从另一个线程读取ab,并且
  • 如果您读取b并查看存储在其中的version,那么
    • 您可以读取d; 和
    • 您所读取的将是value

请注意,第二部分是否正确取决于内存顺序; 它使用默认值(memory_order_seq_cst)是正确的。


如果读者看到a和b的版本相同,那么我不是保证已经读取了使用该版本设置的值吗?据我理解,在调用写入时,除非在a = version之前就能看到d = value行的影响,否则我有这个保证。 - Jeffrey Dalla Tezza
@JeffreyDallaTezza 这里的重点是,除非你知道写入已经完成,否则你无法读取 d,而你通过读取 b 并查看其中是否存储了 version 来实现这一点。你的代码无条件地从 d 读取,这会导致数据竞争和未定义行为。 - T.C.

0

执行 d = value; 的效果是否保证在执行 a = version; 之前不可见,在执行 b = version; 之后可见?

是的,保证。这是因为当读取或写入 atomic<> 变量时,隐含了顺序一致性屏障

在修改值之前和之后,您可以将版本标记存储到一个原子变量中,并递增该变量:

atomic<int> a = {0};
double d;

void write(double value)
{
     a = a + 1; // 'a' become odd
     d = value; //or other modification of protected value(s)
     a = a + 1; // 'a' become even, but not equal to the one before modification
}

double read(void)
{
     int x;
     double ret;
     do
     {
         x = a;
         ret = value; // or other action with protected value(s)
     } while((x & 2) || (x != a));
     return ret;
}

这在Linux内核中被称为seqlockhttp://en.wikipedia.org/wiki/Seqlock

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接