简体   繁体   English

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

[英]Race condition when incrementing and decrementing global variable in C++

I found an example of a race condition that I was able to reproduce under g++ in linux. 我找到了一个可以在Linux的g++下复制的竞争条件的示例。 What I don't understand is how the order of operations matter in this example. 在此示例中,我不了解的是操作顺序的重要性。

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;
}

I can undertand that the order matters if I had used va = va + 1; 我可以理解,如果我使用va = va + 1; ,则顺序很重要va = va + 1; because then RHS va could have changed before getting back to assigned LHS va . 因为这样RHS va可能会在返回分配的LHS va之前发生变化。 Can someone clarify? 有人可以澄清吗?

The standard says (quoting the latest draft): 标准说(引用最新草案):

[intro.races] [介绍种族]

Two expression evaluations conflict if one of them modifies a memory location ([intro.memory]) and the other one reads or modifies the same memory location. 如果两个表达式评估之一修改一个内存位置([intro.memory]),而另一个表达式读取或修改了相同的内存位置,则这两个表达式评估会发生冲突。

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. 如果一个程序的执行包含两个潜在的并发冲突操作,其中至少一个不是原子操作,并且没有一个先于另一个发生,则执行该程序将导致数据争用,除了以下所述的信号处理程序的特殊情况。 Any such data race results in undefined behavior . 任何此类数据争用都会导致不确定的行为

Your example program has a data race, and the behaviour of the program is undefined. 您的示例程序存在数据争用,并且程序的行为未定义。


What I don't understand is how the order of operations matter in this example. 在此示例中,我不了解的是操作顺序的重要性。

The order of operations matters because the operations are not atomic, and they read and modify the same memory location. 操作的顺序很重要,因为操作不是原子的,并且它们读取和修改相同的内存位置。

can undertand that the order matters if I had used va = va + 1; 如果我使用了va = va + 1,可以理解该顺序很重要; because then RHS va could have changed before getting back to assigned LHS va 因为这样RHS VA可能会在返回分配的LHS VA之前发生变化

The same applies to the increment operator. 增量运算符也是如此。 The abstract machine will: 抽象机将:

  • Read a value from memory 从内存中读取一个值
  • Increment the value 增值
  • Write a value back to memory 将值写回内存

There are multiple steps there that can interleave with operations in the other thread. 那里有多个步骤可以与另一个线程中的操作交错。

Even if there was a single operation per thread, there would be no guarantee of well defined behaviour unless those operations are atomic. 即使每个线程只有一个操作,也不能保证行为明确,除非这些操作是原子操作。


Note outside of the scope of C++: A CPU might have a single instruction for incrementing an integer in memory. 请注意,C ++范围之外:CPU可能只有一条指令来增加内存中的整数。 For example, x86 has such instruction. 例如,x86具有这样的指令。 It can be invoked both atomically and non-atomically. 可以原子地或非原子地调用它。 It would be wasteful for the compiler to use the atomic instruction unless you explicitly use atomic operations in C++. 除非您在C ++中显式使用原子操作,否则编译器使用原子指令会很浪费。

The important idea here is that when c++ is compiled it is "translated" to assembly language. 这里的重要思想是,在编译c ++时,它将“翻译”为汇编语言。 The translation of ++va or --va will result in assembly code that moves the value of va to a register, then stores the result of adding 1 to that register back to va in a separate instruction. ++va--va的翻译将产生汇编代码,该代码将va的值移至寄存器,然后在单独的指令中将对该寄存器加1的结果存储回va In this way, it is exactly the same as va = va + 1; 这样,它与va = va + 1;完全相同va = va + 1; . It also means that the operation va++ is not necessarily atomic . 这也意味着操作va++不一定是原子操作

See here for an explanation of what the Assembly code for these instructions will look like. 请参阅此处以获取有关这些指令的汇编代码的外观的说明。

In order to make atomic operations, the variable could use a locking mechanism. 为了进行原子操作,该变量可以使用锁定机制。 You can do this by declaring an atomic variable (which will handle synchronization of threads for you): 您可以通过声明一个原子变量(它将为您处理线程同步)来做到这一点:

std::atomic<int> va;

Reference: https://en.cppreference.com/w/cpp/atomic/atomic 参考: https//en.cppreference.com/w/cpp/atomic/atomic

First of all, this is undefined behaviour since the two threads' reads and writes of the same non-atomic variable va are potentially concurrent and neither happens before the other. 首先,这是未定义的行为,因为两个线程对同一个非原子变量va的读取和写入可能是并发的,并且在两个线程之前都不会发生。

With that being said, if you want to understand what your computer is actually doing when this program is run, it may help to assume that ++va is the same as va = va + 1 . 话虽如此,如果您想了解运行该程序时计算机的实际运行状况,则可能有助于假设++vava = va + 1 In fact, the standard says they are identical, and the compiler will likely compile them identically. 实际上,该标准说它们是相同的,并且编译器可能会以相同的方式编译它们。 Since your program contains UB, the compiler is not required to do anything sensible like using an atomic increment instruction. 由于您的程序包含UB,因此不需要编译器执行任何合理的操作,例如使用原子增量指令。 If you wanted an atomic increment instruction, you should have made va atomic. 如果您想要原子增量指令,则应将va原子。 Similarly, --va is the same as va = va - 1 . 类似地, --va相同va = va - 1 So in practice, various results are possible. 因此在实践中,各种结果都是可能的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM