스레드에서 공유 변수를 변경하는 코드가 경쟁 조건을 겪지 않는 이유는 무엇입니까? return 0; } 라인으로 컴파일 :

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 시스템에서 경합 상태는 경우에 따라 많은 명령이 원자적이고 일관성 보장이 매우 높기 때문에 문제를 거의 일으키지 않습니다. 이러한 보장은 많은 명령어가 원자 적이기 위해 잠금 접두사가 필요한 다중 프로세서 시스템에서 다소 감소합니다.

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

특히이 경우 코드가 단일 프로세서 시스템에서 실제로 원자 적 인 원자 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.


답변

경쟁 조건으로 고통받습니다. 넣어 usleep(1000);전에 u++;에서 foo나는 다른 출력 (<1000) 때마다 참조하십시오.


답변

  1. 경쟁 조건 존재 하더라도 왜 당신에게 나타나지 않았는 지에 대한 대답 foo()은 스레드를 시작하는 데 걸리는 시간에 비해 너무 빠르기 때문에 각 스레드가 다음 스레드가 시작되기 전에 완료됩니다. 그러나…

  2. 원래 버전을 사용해도 결과는 시스템에 따라 다릅니다. (쿼드 코어) Macbook에서 원하는 방식으로 시도했으며 10 회 실행 한 결과 1000 회 3 회, 999 회 6 회, 998 회를 1 회 받았습니다. 따라서 경주는 다소 드물지만 분명히 존재합니다.

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

답변