먼저 언어 변호사처럼 생각하는 법을 배워야합니다.
C ++ 사양은 특정 컴파일러, 운영 체제 또는 CPU를 참조하지 않습니다. 실제 시스템의 일반화 인 추상 기계 를 참조 합니다. 언어 변호사 세계에서 프로그래머의 임무는 추상 기계의 코드를 작성하는 것입니다. 컴파일러의 임무는 콘크리트 기계에서 해당 코드를 실현하는 것입니다. 사양을 엄격하게 코딩하면 코드는 오늘날이든 50 년이든 호환 C ++ 컴파일러를 사용하는 시스템에서 코드를 수정하지 않고 컴파일하고 실행할 수 있습니다.
C ++ 98 / C ++ 03 사양의 추상 머신은 기본적으로 단일 스레드입니다. 따라서 스펙과 관련하여 "완전히 이식 가능한"다중 스레드 C ++ 코드를 작성할 수 없습니다. 스펙은 메모리로드 및 저장 의 원 자성 또는 로드 및 저장이 발생할 수 있는 순서 에 대해 아무 것도 말하지 않으며 뮤텍스와 같은 것을 신경 쓰지 마십시오.
물론 pthread 또는 Windows와 같은 특정 콘크리트 시스템에 실제로 다중 스레드 코드를 작성할 수 있습니다. 그러나 C ++ 98 / C ++ 03을위한 멀티 스레드 코드를 작성하는 표준 방법 은 없습니다 .
C ++ 11의 추상 머신은 설계 상 다중 스레드입니다. 또한 잘 정의 된 메모리 모델을 가지고 있습니다 . 즉, 메모리에 액세스 할 때 컴파일러에서 수행 할 수있는 작업과 수행 할 수없는 작업을 나타냅니다.
두 개의 스레드가 한 쌍의 전역 변수에 동시에 액세스하는 다음 예를 고려하십시오.
Global
int x, y;
Thread 1 Thread 2
x = 17; cout << y << " ";
y = 37; cout << x << endl;
스레드 2가 무엇을 출력 할 수 있습니까?
C ++ 98 / C ++ 03에서 이것은 정의되지 않은 동작도 아닙니다. 표준은 "스레드"라고 불리는 것을 고려하지 않기 때문에 질문 자체는 의미 가 없습니다.
C ++ 11에서는로드 및 저장이 일반적으로 원자 일 필요가 없으므로 결과는 정의되지 않은 동작입니다. 어느 정도 개선되지 않은 것처럼 보일 수 있습니다 ... 그리고 그 자체로는 그렇지 않습니다.
그러나 C ++ 11을 사용하면 다음과 같이 작성할 수 있습니다.
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17); cout << y.load() << " ";
y.store(37); cout << x.load() << endl;
이제 상황이 훨씬 더 흥미로워집니다. 우선, 여기에 동작이 정의되어 있습니다. 스레드 2는 이제 0 0
스레드 1 이전에 실행되는 경우 (스레드 1 37 17
이후에 실행되는 경우) 또는 0 17
스레드 1이 x에 할당되었지만 y에 할당되기 전에 실행 된 경우 인쇄 할 수 있습니다.
37 0
C ++ 11의 원자 적로드 / 스토어에 대한 기본 모드는 순차적 일관성을 유지 하기 때문에 인쇄 할 수 없습니다 . 이는 모든로드와 저장소가 각 스레드 내에서 작성한 순서대로 "있는 것처럼"발생해야한다는 것을 의미하지만 스레드 간의 작업은 인터리브 될 수 있지만 시스템은 좋아합니다. 그래서 아토의 기본 동작은 모두 제공 자성 및 주문 로드 및 저장에 대한합니다.
이제 최신 CPU에서는 순차 일관성을 보장하는 데 많은 비용이들 수 있습니다. 특히, 컴파일러는 여기에서 모든 액세스 사이에 완전한 메모리 장벽을 생성 할 수 있습니다. 그러나 알고리즘이 비 순차적로드 및 저장을 허용 할 수있는 경우 즉, 원 자성이 필요하지만 순서는 필요하지 않은 경우; 즉, 37 0
이 프로그램의 출력으로 허용 할 수 있으면 다음과 같이 작성할 수 있습니다.
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
최신 CPU 일수록 이전 예보다 빠를 가능성이 높습니다.
마지막으로 특정로드 및 스토어를 순서대로 유지해야하는 경우 다음과 같이 작성할 수 있습니다.
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
이로 인해 주문 된로드 및 저장으로 되돌아 가서 37 0
더 이상 가능한 출력은 아니지만 최소한의 오버 헤드로 그렇게됩니다. (이 사소한 예에서 결과는 완전한 순차 일관성과 동일하지만 더 큰 프로그램에서는 그렇지 않습니다.)
물론보고자하는 유일한 출력이 0 0
or 37 17
인 경우 원래 코드를 뮤텍스로 감쌀 수 있습니다. 그러나 당신이 이것을 멀리 읽었다면, 나는 그것이 어떻게 작동하는지 이미 알고 있으며,이 대답은 이미 의도했던 것보다 깁니다 :-).
결론입니다. 뮤텍스는 훌륭하고 C ++ 11은 그것들을 표준화합니다. 그러나 때때로 성능상의 이유로 하위 레벨 기본 요소 (예 : 클래식 이중 검사 잠금 패턴 ) 를 원합니다 . 새로운 표준은 뮤텍스 및 조건 변수와 같은 고급 장치를 제공하며 원자 유형 및 다양한 메모리 장벽과 같은 하위 장치를 제공합니다. 이제 표준에 지정된 언어 내에서 정교한 고성능 동시 루틴을 작성할 수 있으며 오늘날 시스템과 내일의 코드에서 코드가 컴파일되고 변경되지 않을 것입니다.
솔직하지만 전문가가 아니고 심각한 저수준 코드를 작성하지 않는 한 뮤텍스와 조건 변수를 고수해야합니다. 그것이 내가하려는 일입니다.
이 내용에 대한 자세한 내용은 이 블로그 게시물을 참조하십시오 .