当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《为什么代码在线程之间改变共享变量显然不会受到竞争条件的影响?》经验,为你挑选了5个好方法。

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

#include 
#include 
#include 
using namespace std;

unsigned u = 0;

void foo()
{
    u++;
}

int main()
{
    vector 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;
}

Rob K.. 266

foo()是如此之短,每个线程可能会在下一个线程产生之前完成.如果您在foo()之前的随机时间内添加睡眠u++,您可能会开始看到您的期望.



1> Rob K..:

foo()是如此之短,每个线程可能会在下一个线程产生之前完成.如果您在foo()之前的随机时间内添加睡眠u++,您可能会开始看到您的期望.


这确实以预期的方式改变了输出.
我会注意到,这通常是展示竞争条件的一个相当好的策略.您应该可以在任意两个操作之间注入暂停; 如果没有,那就是竞争条件.

2> Vality..:

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

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

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

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

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

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

这意味着它只是执行一个添加指令,对于单个处理器来说它将是原子的(尽管如上所述对于多处理器系统不是这样).


重要的是要记住"按预期运行"是未定义行为的允许结果.
正如您所指出的,该指令在SMP机器上并非*原子*(所有现代系统都是如此).甚至`inc [u]`也不是原子的.需要`LOCK`前缀才能使指令真正成为原子.OP很幸运.回想一下,即使你告诉CPU"在这个地址加1",CPU仍然需要获取,增加,存储该值,而另一个CPU可以同时执行相同的操作,导致结果不正确.
Downvoted,我发现这个说法有点meh*"特别是在X86和AMD64机器上,竞争条件在某些情况下很少会引起问题,因为许多指令都是原子的,并且一致性保证非常高."*段落应该开始明确假设你专注于单核心.即便如此,多核架构现在在消费类设备中已成为事实上的标准,我认为这是一个最后解释的角落,而不是第一个.
哦,当然.x86有很多向后兼容性的东西,以确保错误编写的代码尽可能地工作.当Pentium Pro引入无序执行时,这是一个非常重要的事情.英特尔希望确保已安装的代码基础*无需*需要专门为其新芯片重新编译.x86最初是作为CISC核心开发的,但内部已演变为RISC核心,尽管从程序员的角度来看它仍以多种方式呈现为CISC.有关更多信息,请参阅Peter Cordes的回答[此处](http://stackoverflow.com/q/39393850).
我投票了,但后来我重新阅读了你的问题,并意识到你的原子性陈述假定是一个单独的CPU.如果你编辑你的问题以使其更清楚(当你说"原子"时,要明确这只是单个CPU的情况),那么我将能够删除我的投票.
嗯,不.这是错的.`添加DWORD PTR u [rip],1`或**任何带有内存操作数的指令在x86**上都是*not*atomic.`add rax,1`(或任何带有寄存器操作数的指令)都是原子的,但是当你有一个内存操作数时,指令被分解成~3个微操作(在这种情况下是一个加载,一个加法和一个存储) ),这意味着它不是原子的.我猜你有点尝试,并且说它在单CPU系统上只是原子的,但这没有多大意义.它仍然是*不是原子*,根据定义,并且有多个线程,你仍然没有`lock`前缀.
确保你重复几遍@Vality,但这只是从第三段开始.这样一个重要的细节应该是你的答案的开场陈述,而不是隐藏在中途,因为它使前半部分混乱无缘无故.

3> Stephan Lech..:

我认为如果你在睡前或睡觉后睡觉,那就不是那么重要了u++.更确切地说,操作u++转换为代码 - 与产生调用的线程的开销相比foo- 非常快速地执行,使得它不太可能被截获.但是,如果你"延长"操作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.


从输出看,它看起来像`其他u - = 1`执行一次,第一次调用foo(),当u == 0.剩余的999次u是奇数并且执行'u + = 2`在u = -1 + 999*2 = 1997; 即正确的输出.竞争条件有时会导致+ = 2中的一个被并行线程覆盖而你会得到1995.

4> 小智..:

它确实患有竞争条件.放在usleep(1000);之前u++;,foo我每次看到不同的输出(<1000).



5> 小智..:

    虽然它确实存在,但为什么竞争条件没有为你显现的可能答案foo()是,与启动一个线程所花费的时间相比,它是如此之快,每个线程在下一个线程开始之前完成.但...

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

    你编译'-g',有一种方法可以让bug消失.我重新编译了你的代码,但仍然没有改变'-g',并且比赛变得更加明显:我有1000次,999次,998次,997次,996次,992次.

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

    #include 
    #include 
    #include 
    using namespace std;
    
    unsigned u = 0;
    bool start = false;
    
    void foo()
    {
        while (!start) {
            std::this_thread::yield();
        }
        u++;
    }
    
    int main()
    {
        vector 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;
    }
    

推荐阅读
贾志军
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有