在C++中递增和递减全局变量时的竞争条件

在C++中递增和递减全局变量时的竞争条件,c++,concurrency,race-condition,C++,Concurrency,Race Condition,我发现了一个竞争条件的例子,我可以在linux的g++下重现。我不明白的是,在这个例子中,操作顺序是如何重要的 int va = 0; void fa() { for (int i = 0; i < 10000; ++i) ++va; } void fb() { for (int i = 0; i < 10000; ++i) --va; } int main() { std::thread a(fa); std::

我发现了一个竞争条件的例子,我可以在linux的g++下重现。我不明白的是,在这个例子中,操作顺序是如何重要的

int va = 0;

void fa() {
    for (int i = 0; i < 10000; ++i)
        ++va;
}

void fb() {
    for (int i = 0; i < 10000; ++i)
        --va;
}

int main() {
    std::thread a(fa);
    std::thread b(fb);
    a.join();
    b.join();
    std::cout << va;
}

如果我使用va=va+1,我可以理解顺序很重要;因为RHS va在返回指定的LHS va之前可能已经更改。有人能澄清一下吗?

标准中引用了最新的草案:

[比赛简介]

如果其中一个修改内存位置[intro.memory],而另一个读取或修改同一内存位置,则两个表达式求值会发生冲突

如果一个程序的执行包含两个潜在的并发冲突动作,则该程序的执行包含一个数据竞争,其中至少一个动作不是原子的,并且两个动作都不在另一个动作之前发生,下面描述的信号处理程序的特殊情况除外。 任何这样的数据竞争都会导致未定义的行为

您的示例程序存在数据竞争,并且程序的行为未定义

我不明白的是,在这个例子中,操作顺序是如何重要的

int va = 0;

void fa() {
    for (int i = 0; i < 10000; ++i)
        ++va;
}

void fb() {
    for (int i = 0; i < 10000; ++i)
        --va;
}

int main() {
    std::thread a(fa);
    std::thread b(fb);
    a.join();
    b.join();
    std::cout << va;
}
操作的顺序很重要,因为操作不是原子的,它们读取和修改相同的内存位置

如果我使用va=va+1,可以理解顺序的重要性;因为在返回到指定的LHS va之前,RHS va可能已经改变

这同样适用于增量运算符。抽象机器将:

从内存中读取一个值 增值 将值写回内存 有多个步骤可以与另一个线程中的操作交错

即使每个线程只有一个操作,也不能保证定义良好的行为,除非这些操作是原子操作


注意:在C++的范围之外:CPU可能有一条指令来递增内存中的整数。例如,x86有这样的指令。它可以以原子方式和非原子方式调用。除非你在C++中明确地使用原子操作,否则,编译器将浪费原子指令。

这里的重要思想是,当C++被编译时,它被翻译成汇编语言。++va或-va的转换将产生汇编代码,该代码将va的值移动到寄存器,然后将向该寄存器添加1的结果存储到单独的指令中。这样,它与va=va+1;完全相同;。这也意味着操作va++不一定是原子的

有关这些说明的汇编代码的说明,请参阅

为了进行原子操作,变量可以使用锁定机制。您可以通过声明一个原子变量来实现这一点,该变量将为您处理线程同步:

std::atomic<int> va;

参考资料:

首先,这是未定义的行为,因为两个线程对同一个非原子变量va的读写可能是并发的,并且两个线程都不会在另一个线程之前发生


话虽如此,如果您想了解运行此程序时计算机实际在做什么,那么假定++va与va=va+1相同可能会有所帮助。事实上,标准上说它们是相同的,编译器很可能会以相同的方式编译它们。由于您的程序包含UB,编译器不需要执行任何合理的操作,比如使用原子增量指令。如果你想要一个原子增量指令,你应该让va原子化。类似地,-va与va=va-1相同。因此,在实践中,各种结果都是可能的。

不确定需要澄清什么。你自己说这是一场比赛。种族引入未定义的行为。任何事情都可能发生。两个线程都试图同时修改va,甚至在for循环内部。谁赢了?现在还不知道,所以va的最终值可以是任何东西。我怀疑你相信递增和递减操作是原子的。它们不是。即使操作是原子的,如果没有同步,也无法保证对va的更改是否或何时被推送到其他线程可以看到的地方。例如,va可能很好地存储在寄存器中。顺便说一句,优化器可以将这些方法更改为va+=10000;,va+=10000;,减少更多看到种族效应的机会。我认为你的第一部分有点误导。UB可能会导致总成某一天看起来很好,但另一天却不行day@formerlyknownas_463035818我试图通过强调C++操作是非原子的来使这一点更清楚,因为底层的汇编指令执行get和SETAL。实际上,现在我才明白你在说什么;1.