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

sor*_*bro 0 c++ concurrency race-condition

我发现了一个竞争条件的例子,我可以g++在 Linux 下重现它。我不明白的是,在此示例中操作顺序有何影响。

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;
}
Run Code Online (Sandbox Code Playgroud)

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

eer*_*ika 5

该标准说(引用最新草案):

[种族介绍]

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

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

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


我不明白的是,在此示例中操作顺序有何影响。

操作的顺序很重要,因为这些操作不是原子的,并且它们读取和修改相同的内存位置。

如果我使用 va = va + 1; 就可以理解顺序很重要;因为这样 RHS va 可能会在返回指定的 LHS va 之前发生变化

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

  • 从内存中读取一个值
  • 增加值
  • 将值写回内存

其中有多个步骤可以与另一个线程中的操作交错。

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


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