developer tip

스레드에서 공유 변수를 변경하는 코드가 왜 경쟁 조건을 겪지 않는 것입니까?

optionbox 2020. 8. 6. 08:15
반응형

스레드에서 공유 변수를 변경하는 코드가 왜 경쟁 조건을 겪지 않는 것입니까?


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()너무 짧아서 다음 스레드가 생성되기 전에 각 스레드가 완료 될 수 있습니다. foo()앞에 임의의 시간 동안 수면을 추가 u++하면 예상 한 것을 볼 수 있습니다.


경쟁 조건이 코드가 잘못 실행된다는 것을 보장하지는 않으며, 정의되지 않은 동작이기 때문에 코드가 무엇이든 할 수 있다는 것을 이해하는 것이 중요합니다. 예상대로 달리는 것을 포함합니다.

특히 X86 및 AMD64 시스템의 경합 조건에서는 많은 명령이 원자적이고 일관성 보장이 매우 높기 때문에 거의 문제를 일으키지 않습니다. 이러한 보장은 많은 명령어가 원 자성이되기 위해 잠금 접두사가 필요한 다중 프로세서 시스템에서는 다소 줄어 듭니다.

컴퓨터에서 증분이 원자 연산 인 경우 언어 표준에 따라 정의되지 않은 동작 인 경우에도 올바르게 실행됩니다.

Specifically I expect in this case the code may be being compiled to an atomic Fetch and Add instruction (ADD or XADD in X86 assembly) which is indeed atomic in single processor systems, however on multiprocessor systems this is not guaranteed to be atomic and a lock would be required to make it so. If you are running on a multiprocessor system there will be a window where threads could interfere and produce incorrect results.

Specifically I compiled your code to assembly using https://godbolt.org/ and foo() compiles to:

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

This means it is solely performing an add instruction which for a single processor will be atomic (though as mentioned above not so for a multi processor system).


I think it is not so much the thing if you put a sleep before or after the u++. It is rather that operation u++ translates to code that is - compared to the overhead of spawning threads that call foo - very quickly performed such that it is unlikely to get intercepted. However, if you "prolong" the operation u++, then the race condition will become much more likely:

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

result: 694


BTW: I also tried

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

and it gave me most times 1997, but sometimes 1995.


It does suffer from a race condition. Put usleep(1000); before u++; in foo and I see different output (< 1000) each time.


  1. The likely answer to why the race condition didn't manifest for you, though it does exist, is that foo() is so fast, compared to the time it takes to start a thread, that each thread finishes before the next can even start. But...

  2. Even with your original version, the result varies by system: I tried it your way on a (quad-core) Macbook, and in ten runs, I got 1000 three times, 999 six times, and 998 once. So the race is somewhat rare, but clearly present.

  3. You compiled with '-g', which has a way of making bugs disappear. I recompiled your code, still unchanged but without the '-g', and the race became much more pronounced: I got 1000 once, 999 three times, 998 twice, 997 twice, 996 once, and 992 once.

  4. Re. the suggestion of adding a sleep -- that helps, but (a) a fixed sleep time leaves the threads still skewed by start time (subject to timer resolution), and (b) a random sleep spreads them out when what we want is to pull them closer together. Instead, I'd code them to wait for a start signal, so I can create them all before letting them get to work. With this version (with or without '-g'), I get results all over place, as low as 974, and no higher than 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;
    }
    

참고URL : https://stackoverflow.com/questions/41816495/why-does-code-mutating-a-shared-variable-across-threads-apparently-not-suffer-fr

반응형