为什么跨线程更改共享变量的代码显然没有受到竞争条件的影响



我正在使用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;
}

用以下行编译:g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o

它打印1000,这是正确的。然而,由于线程覆盖了以前增加的值,我预计会有一个较小的数字。为什么这个代码不受相互访问的影响?

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

当用更复杂的东西(例如)替换共享foo的内容时,问题仍然存在

if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}

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

重要的是要理解竞争条件并不能保证代码会错误运行,只是它可以做任何事情,因为这是一种未定义的行为。包括按预期运行。

特别是在X86和AMD64机器上,竞争条件在某些情况下很少会导致问题,因为许多指令都是原子指令,并且一致性保证非常高。在多处理器系统上,这些保证在一定程度上减少了,因为许多指令都需要锁前缀作为原子。

如果在您的机器上增量是一个原子操作,即使根据语言标准它是Undefined Behavior,它也可能正确运行。

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

具体来说,我使用https://godbolt.org/foo()编译为:

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

这意味着它只执行加法指令,对于单个处理器来说,加法指令将是原子指令(尽管如上所述,对于多处理器系统来说并非如此)。

我认为,如果你在u++之前或之后睡一觉,那就不算什么了。相反,操作u++转换成的代码——与调用foo的生成线程的开销相比——执行得非常快,因此不太可能被拦截。然而,如果你"延长"操作u++,那么比赛条件将变得更有可能:

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

结果:694


BTW:我也试过

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

它给我的时间大多是1997,但有时是1995

它确实受到种族条件的影响。在foo中将usleep(1000);放在u++;之前,我每次看到不同的输出(<1000)。

  1. 尽管竞争条件确实存在,但为什么它没有为您显现,可能的答案是foo()与启动线程所需的时间相比非常快,每个线程甚至在下一个线程开始之前就完成了。但是

  2. 即使是你的原始版本,结果也因系统而异:我在(四核)Macbook上按你的方式尝试了一下,在十次运行中,我三次获得1000,六次获得999,一次获得998。因此,这场比赛有些罕见,但显然存在。

  3. 您使用'-g'进行编译,它有一种使bug消失的方法。我重新编译了你的代码,仍然没有改变,但没有'-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;
    }
    

最新更新