2016-09-10 3 views
5

나는 std::atomic<>을 사용하지 않는 것이 좋지 않다는 것을 보여주고 싶지만 실패를 재생산하는 예제를 생성 할 수는 없습니다.멀티 스레딩에서 원자 유형이 필요합니까? (OS X, clang, C++ 11)

{ 
    foobar = false; 
} 

다른 : 나는 두 개의 스레드가 그 중 하나가하는이

{ 
    if (foobar) { 
     // ... 
    } 
} 

foobar의 유형이 중 bool 또는 std::atomic_bool을 그리고 그것은 true으로 초기화합니다. 나는 OS X Yosemite를 사용 중이며 this 트릭을 사용하여 스레드가 다른 코어에서 실행되도록하려는 CPU 선호도를 통해 힌트를 표시하려고합니다. 루프 등에서 이러한 작업을 실행하고 어떤 경우에도 실행시 관찰 가능한 차이가 없습니다. 나는 그 소리 clang -std=c++11 -lstdc++ -O3 -S test.cpp로 생성 된 어셈블리를 검사 결국하지 난 (오른쪽과 왼쪽에 원자없이) 읽기에 ASM의 차이가 작은 것을 볼 :

enter image description here

없음 mfence 또는 뭔가 "극적인"를. 쓰기 측면에서, 뭔가 더 "극적인"발생 : 당신이 볼 수 있듯이

enter image description here

atomic<> 버전은 암시 적 잠금을 사용 xchgb를 사용합니다. 비교적 오래된 버전의 gcc (v4.5.2)를 사용하여 컴파일 할 때 모든 종류의 mfence이 추가되어 심각한 문제가 있음을 알 수 있습니다.

"X86은 매우 강력한 메모리 모델을 구현합니다"(ref), mfence은 필요하지 않을 수도 있지만 크로스 플랫폼 코드를 작성하지 않는 한 이해할 수 있습니다. ARM을 지원하기 때문에, ns 레벨에서 일관성을 유지하지 않는다면, 실제로는 atomic<>을 넣을 필요가 없습니다.

나는 Herb Sutter에서 "atomic<> Weapons"을 지켜 봤지만 여전히 그 문제를 재현하는 간단한 예제를 만드는 것이 얼마나 어려운지에 감명 받았습니다.

+0

당신의 부울은 실제로 여러 스레드에서 액세스 할 수 있습니까? 관련 세부 사항이 누락 된 스 니펫 일뿐만 아니라 전체 예제 프로그램을 제공하는 것이 가장 좋습니다! 또한 gcc 4.5는 다소 오래된 것이지만 최적화 프로그램은 그 이후로 발전해 왔습니다. – hyde

+2

일반적으로 프로그래밍 언어의 규칙은 결코 당신에게 문제를 일으키는 확실한 방법을 제공하지 않습니다. 그것은 다른 방향입니다 : 규칙은 당신에게 문제가없는 * 보장 * 된 프로그램을 만드는 방법만을 제공합니다. –

+0

당신이 원하는 것은 단순히 "서로 다른 스레드가 동기화가없는 동일한 변수/힙 영역을 사용하는 것은 매우 나쁜 생각"이라는 것을 증명하는 것이 아닙니까? 그렇다면 "표준이 아니다 : 원자력은 매우 나쁜 생각이다"라고 번역하지 않는다. – einpoklum

답변

5

데이터 경주의 큰 문제는 정의되지 않은 동작이며 잘못된 동작이 보장되지 않는다는 것입니다. 그리고 이것은 스레드의 일반적인 예측 불가능 성 및 x64 메모리 모델의 힘과 함께 재현 가능한 오류를 생성하는 것이 실제로 어려움을 의미합니다.

옵티마이 저는 예상치 못한 일을 어셈블리에서 관찰 할 수 있기 때문에 조금 더 안정적인 실패 모드입니다. 물론 옵티마이 저는 악명이 높고 코드 라인을 하나만 변경하면 완전히 다른 것을 할 수도 있습니다.

다음은 코드에서 우리가 한 번 실패한 예입니다. 이 코드는 일종의 스핀 록을 구현했지만 원자를 사용하지 않았습니다.

bool operation_done; 
void thread1() { 
    while (!operation_done) { 
    sleep(); 
    } 
    // do something that depends on operation being done 
} 
void thread2() { 
    // do the operation 
    operation_done = true; 
} 

디버그 모드에서 정상적으로 작동하지만 릴리스 빌드가 작동하지 않습니다. 디버깅은 thread1의 실행이 결코 루프를 벗어나지 않고 어셈블리를 살펴본 결과 조건이 없어 졌음을 발견했습니다. 루프는 단순히 무한했다.

문제는 옵티마이 저가 메모리 모델에서 operation_done이 루프 내에서 변경 될 수 없다는 것을 깨달았 기 때문에 (데이터 경쟁이었을 가능성이 있음) 한 번 조건이 참이되면이를 알 수있었습니다 영원한 것이 될 것입니다.

operation_done 유형을 atomic_bool (또는 실제로는 C++ 11 이전 컴파일러 관련 기능)으로 변경하면 문제가 해결되었습니다.

+5

@ gnasher729 C++에서는 휘발성이 아니라 무엇을 의미합니까? 글래스 하우스. –

+0

@SebastianRedl : 나는 실제로 문서를 읽을 때까지 gnasher729가 옳았다는 잘못된 인상을 받았습니다. 아마도 왜 휘발성이 여기에서하지 않는지 설명하는 문장을 답안에 추가 할 수 있습니다. – einpoklum

+0

@einpoklum : OP에서 '휘발성'이 제기되지 않았기 때문에 실제로 그것을 격추시키기 위해 아이디어를 도입하는 것은 비생산적이었습니다. – Hurkyl

-9

일반적으로 원자 유형을 사용하면 실제로는 다중 스레드 상황에서 유용하게 사용됩니다. 뮤텍스, 세마포어 등을 구현하는 것이 더 유용합니다.

매우 유용하지 않은 한 가지 이유 : 두 값이 원자 적 방식으로 변경되어야하는 즉시, 당신은 절대적으로 붙어 있습니다. 원자 적 가치로는 그것을 할 수 없습니다. 원자 단위로 단일 값을 변경하려는 경우는 드뭅니다.

iOS와 MacOS X에서 사용하는 세 가지 방법은 다음과 같습니다. @synchronized를 사용하여 변경 보호. 순차적 큐 (메인 큐일 수 있음)에서 코드를 실행하여 멀티 스레드 액세스를 피하십시오. 뮤텍스 사용하기.

부울 값에 대한 원 자성이 무의미하다는 점을 알고 계시기 바랍니다. 당신은 가지고있는 경쟁 조건입니다 : 하나의 스레드는 값을 저장하고, 다른 스레드는 값을 저장합니다. 원자력은 여기서 차이를 만들지 않습니다. 변수 에 액세스하는 두 스레드가 정확히 같은 시간에에 액세스하면 문제가 발생합니다. 예를 들어, 변수가 두 개의 스레드에서 정확히 같은 시간에 증가하면 최종 결과가 2 씩 증가한다고 보장됩니까? 원 자성 (또는 앞서 언급 한 방법 중 하나)이 필요합니다.

세바스찬은 원 자성이 데이터 경쟁을 수정한다는 어리석은 주장을합니다. 그건 말도 안됩니다. 데이터 경주에서 독자는 변경 전이나 후에 값을 읽습니다. 값이 원자인지 여부에 관계없이 아무런 차이가 없습니다. 독자는 이전 값 또는 새 값을 읽으므로 동작을 예측할 수 없습니다. 원 자성이하는 일은 독자가 어떤 중간 상태를 읽는 상황을 방지하는 것입니다. 어떤 데이터 경주를 해결하지 않습니다.

+0

일반적으로 원자 데이터 유형은 원자 적으로이를 테스트하고 수정하는 경우 연산을 업데이트하는 메소드를 제공합니다. – hyde

+2

원자는 데이터 경주를 수정하여 전혀 차이가 나지 않습니다 (항상 표시되는 것은 아닐지라도). 더 큰 그림 경쟁 조건이 있는지 여부는 게시 된 스 니펫에서 볼 수 없습니다. –

+0

@hyde : 나는 그들이하는 일을 말하지 않았으며, 당신에게 도움이되는 일은 거의하지 않는다고 말했습니다. – gnasher729

1

이 질문은 @Sebastian Redl의 답변에 대한 나의 고유 버전입니다. 나는 컴파일러가 글쓰기에 동기화를 추가하는 것을 보자 마자 모든 문제를 해결할 수있는 문제가 생기지 않았기 때문에 모든 것을 명확하게 한 글에 다시주의를 환기시킨 @HansPassant에 대한 신용 + 명성을 여전히 받아 들일 것입니다. bool을 예상 한만큼 최적화하지 마십시오.

std::atomic_bool foobar(true); 
//bool foobar = true; 

long long cnt = 0; 
long long loops = 400000000ll; 

void thread_1() { 
    usleep(200000); 
    foobar = false; 
} 

void thread_2() { 
    while (loops--) { 
     if (foobar) { 
      ++cnt; 
     } 
    } 
    std::cout << cnt << std::endl; 
} 

내 원래의 코드와 가장 큰 차이점은 내가 while 루프 내부 usleep()을하는 데 사용했다 :

나는 문제를 재현 사소한 프로그램을 할 수 있었다. while 루프 내에서 최적화를 방지하는 것으로 충분했습니다.

enter image description here

하지만 읽기에 매우 다른 : : 위의 청소기 코드 쓰기를 같은 ASM 산출 우리는 bool 경우에 그것을 볼 수 있습니다

enter image description here

(왼쪽) 그 소리가 가져 if (foobar)이 루프 외부에 있습니다.나는 bool 케이스를 실행 따라서 때 얻을 : 나는 atomic_bool 케이스를 실행할 때

400000000 

real 0m1.044s 
user 0m1.032s 
sys 0m0.005s 

동안 내가 얻을 :

95393578 

real 0m0.420s 
user 0m0.414s 
sys 0m0.003s 

그것은 atomic_bool 경우가 빠르다는 것을 재미있다 - 그냥 95 않기 때문에 내가 추측 400000000 반대로 카운터에 대한 bool 케이스에 inc 만.

그래도 이보다 더 재미있는 것은 무엇입니까? 내가 pthread_join() 후 스레드 코드 밖으로 std::cout << cnt << std::endl;를 이동하는 경우, 비 원자 경우 루프 그냥이된다 :

enter image description here

즉 더 루프가 없습니다. 그냥 if (foobar!=0) cnt = loops;입니다! 영리한 clang. 그런 다음 실행은

400000000 

real 0m0.206s 
user 0m0.001s 
sys 0m0.002s 

이고, atomic_bool은 동일하게 유지됩니다.

따라서 atomic을 사용해야한다는 충분한 증거가 있습니다. 기억할 수있는 유일한 점은 - 작은 경우에도 컴파일러 최적화를 막을 수 있기 때문에 벤치 마크에 usleep()을 넣지 마십시오.

관련 문제