为什么代码在线程之间改变共享变量显然不会受到竞争条件的影响?

时间:2017-01-23 22:02:13

标签: c++ race-condition

我正在使用Cygwin GCC并运行此代码:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

unsigned u = 0;

void foo()
{
    u++;
}

int main()
{
    vector<thread> threads;
    for(int i = 0; i < 1000; i++) {
        threads.push_back (thread (foo));
    }
    for (auto& t : threads) t.join();

    cout << u << endl;
    return 0;
}

使用以下行编译:{{1​​}}。

打印1000,这是正确的。但是,由于线程覆盖了先前增加的值,我预计数量会减少。为什么这段代码不会受到相互访问的影响?

我的测试机器有4个核心,我对我所知道的程序没有任何限制。

将共享g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o的内容替换为更复杂的内容时,问题仍然存在,例如。

foo

5 个答案:

答案 0 :(得分:267)

foo()非常短,以至于每个线程可能在下一个线程生成之前完成。如果您在foo()之前的u++中随机添加一次睡眠,您可能会开始看到您的期望。

答案 1 :(得分:59)

重要的是要了解竞争条件不能保证代码运行不正确,只是它可以做任何事情,因为它是一个未定义的行为。包括按预期运行。

特别是在X86和AMD64机器上,竞争条件在某些情况下很少会引起问题,因为许多指令都是原子的,而且一致性保证非常高。在多处理器系统上,这些保证有所减少,其中许多指令需要锁定前缀才是原子的。

如果你的机器增量是一个原子操作,即使根据语言标准它也是未定义的行为,这可能会正确运行。

具体来说,我希望在这种情况下代码可能被编译为原子Fetch and Add指令(X86程序集中的ADD或XADD),这在单处理器系统中确实是原子的,但是在多处理器系统上,这不保证是原子的,需要一个锁来做到这一点。如果您在多处理器系统上运行,则会出现一个窗口,其中线程可能会干扰并产生不正确的结果。

具体来说,我使用https://godbolt.org/将您的代码编译为程序集,foo()编译为:

foo():
        add     DWORD PTR u[rip], 1
        ret

这意味着它只执行一个add指令,对于单个处理器来说它将是原子的(尽管上面提到的多处理器系统并非如此)。

答案 2 :(得分:20)

我认为如果你在u++之前或之后进行睡眠,那就不是那么重要了。更确切地说,操作u++转换为代码 - 与生成调用foo的线程的开销相比 - 非常快速地执行,使得它不太可能被截获。但是,如果你&#34;延长&#34;操作u++,那么竞争条件将更有可能:

void foo()
{
    unsigned i = u;
    for (int s=0;s<10000;s++);
    u = i+1;
}

结果:694

顺便说一句:我也试过了

if (u % 2) {
    u += 2;
} else {
    u -= 1;
}

它给了我大部分时间1997,但有时1995

答案 3 :(得分:7)

它确实遭受了竞争条件。将usleep(1000);放在u++; foo之前,每次都会看到不同的输出(<1000)。

答案 4 :(得分:6)

  1. 虽然确实存在,但为什么竞争条件没有显现出来的可能答案是foo()与所需的时间相比如此之快启动一个线程,每个线程在下一个线程完成之前甚至可以启动。但是......

  2. 即使使用原始版本,结果也会因系统而异:我在(四核)Macbook上尝试过,在十次运行中,我得到1000次,9次,6次,998次, 。所以比赛有点罕见,但显然存在。

  3. 您使用'-g'编译,这有一种方法可以使错误消失。我重新编译了你的代码,但没有改变,但没有'-g',比赛变得更加明显:我有1000次,999次,998次,997次,996次,992次。

  4. 重新。添加睡眠的建议 - 这有帮助,但是(a)固定的睡眠时间使得线程仍然在开始时间(受定时器分辨率影响)的情况下偏斜,以及(b)当我们想要的是随机睡眠时将它们展开拉近他们。相反,我会将它们编码为等待启动信号,因此我可以在让它们开始工作之前创建它们。使用此版本(有或没有'-g'),我得到的结果全部到位,低至974,不高于998:

    #include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    unsigned u = 0;
    bool start = false;
    
    void foo()
    {
        while (!start) {
            std::this_thread::yield();
        }
        u++;
    }
    
    int main()
    {
        vector<thread> threads;
        for(int i = 0; i < 1000; i++) {
            threads.push_back (thread (foo));
        }
        start = true;
        for (auto& t : threads) t.join();
    
        cout << u << endl;
        return 0;
    }