etc./StackOverFlow

C++11은 표준화된 메모리 모델을 도입했습니다. 무슨 뜻이에요? 그리고 C++ 프로그래밍에 어떤 영향을 미칠까요?

청렴결백한 만능 재주꾼 2021. 12. 4. 10:39
반응형

질문자 :Nawaz


C++11은 표준화된 메모리 모델을 도입했지만 정확히 무엇을 의미합니까? 그리고 C++ 프로그래밍에 어떤 영향을 미칠까요?

Herb Sutter 를 인용한 Gavin Clark의 이 기사에서는 다음과 같이 말합니다.

메모리 모델은 C++ 코드가 이제 컴파일러를 만든 사람과 실행 중인 플랫폼에 관계없이 호출할 표준화된 라이브러리를 가짐을 의미합니다. 서로 다른 스레드가 프로세서의 메모리와 통신하는 방식을 제어하는 표준 방법이 있습니다.

"표준에 있는 서로 다른 코어에 [코드]를 분할하는 것에 대해 이야기할 때 우리는 메모리 모델에 대해 이야기하는 것입니다. 우리는 사람들이 코드에서 만들려는 다음 가정을 깨지 않고 최적화할 것입니다. "라고 Sutter가 말했습니다.

글쎄, 나는 온라인에서 사용할 수 있는 이 단락과 유사한 단락을 외울 수 있고(나는 태어날 때부터 내 자신의 기억 모델을 가지고 있었기 때문에 :P) 다른 사람들이 묻는 질문에 대한 답변으로 게시할 수도 있지만 솔직히 말해서 정확히 이해하지 못합니다. 이것.

C++ 프로그래머는 이전에도 다중 스레드 응용 프로그램을 개발하는 데 사용했는데 POSIX 스레드, Windows 스레드 또는 C++11 스레드 중 어떤 것이 중요합니까? 혜택은 무엇입니까? 낮은 수준의 세부 사항을 이해하고 싶습니다.

나는 또한 C++11 메모리 모델이 C++11 멀티 스레딩 지원과 어떤 식으로든 관련되어 있다는 느낌을 받습니다. 그렇다면 정확히 어떻게? 왜 관련이 있어야 합니까?

멀티 스레딩의 내부가 어떻게 작동하는지, 일반적으로 메모리 모델이 무엇을 의미하는지 모르기 때문에 이러한 개념을 이해하는 데 도움을 주세요. :-)



첫째, 언어 변호사처럼 생각하는 법을 배워야 합니다.

C++ 사양은 특정 컴파일러, 운영 체제 또는 CPU를 참조하지 않습니다. 실제 시스템을 일반화한 추상 기계를 참조합니다. Language Lawyer 세계에서 프로그래머의 임무는 추상 기계를 위한 코드를 작성하는 것입니다. 컴파일러의 임무는 그 코드를 구체적인 기계에서 구현하는 것입니다. 사양에 따라 엄격하게 코딩하면 오늘이든 앞으로 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보다 먼저 실행되는 경우), 37 17 (스레드 1 이후에 실행되는 경우) 또는 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 또는 37 17 인 경우 원래 코드 주위에 뮤텍스를 감쌀 수 있습니다. 그러나 여기까지 읽었다면 이미 어떻게 작동하는지 알고 있을 것이며 이 답변은 이미 내가 의도한 것보다 더 깁니다 :-).

결론은. 뮤텍스는 훌륭하고 C++11은 이를 표준화합니다. 그러나 때로는 성능상의 이유로 더 낮은 수준의 기본 요소(예: 고전적인 이중 확인 잠금 패턴 )를 원합니다. 새로운 표준은 뮤텍스 및 조건 변수와 같은 고수준 가젯을 제공하며 원자 유형 및 다양한 메모리 장벽과 같은 저수준 가젯도 제공합니다. 따라서 이제 표준에서 지정한 언어 내에서 정교한 고성능 동시 실행 루틴을 완전히 작성할 수 있으며 현재의 시스템과 미래의 시스템 모두에서 코드가 변경되지 않고 컴파일되고 실행될 것이라고 확신할 수 있습니다.

솔직히 말해서, 전문가가 아니고 심각한 저수준 코드 작업을 하지 않는 한 아마도 뮤텍스와 조건 변수를 고수해야 할 것입니다. 그것이 내가 하려는 것입니다.

이 항목에 대한 자세한 내용은 이 블로그 게시물 을 참조하십시오.


Nemo

나는 단지 메모리 일관성 모델(또는 줄여서 메모리 모델)을 이해하는 비유를 제공할 것입니다. Leslie Lamport의 획기적인 논문 "Time, Clocks, and Ordering of Events in a Distributed System"에서 영감을 받았습니다. 비유는 적절하고 근본적인 의미가 있지만 많은 사람들에게 과도할 수 있습니다. 그러나 기억 일관성 모델에 대한 추론을 용이하게 하는 정신적 이미지(그림 표현)를 제공하기를 바랍니다.

가로 축이 주소 공간을 나타내고(즉, 각 메모리 위치가 해당 축의 한 점으로 표시됨) 세로 축이 시간을 나타내는 시공간 다이어그램에서 모든 메모리 위치의 기록을 살펴보겠습니다. 일반적으로 시간에 대한 보편적인 개념은 없습니다). 따라서 각 메모리 위치가 보유한 값의 이력은 해당 메모리 주소의 세로 열로 표시됩니다. 각 값 변경은 스레드 중 하나가 해당 위치에 새 값을 쓰기 때문에 발생합니다. 메모리 이미지 는 특정 스레드 가 특정 시간 에 관찰할 수 있는 모든 메모리 위치 값의 집계/결합을 의미합니다.

"메모리 일관성 및 캐시 일관성에 대한 입문서" 인용

직관적인(가장 제한적인) 메모리 모델은 다중 스레드 실행이 단일 코어 프로세서에서 스레드가 시간 다중화된 것처럼 각 구성 스레드의 순차적 실행을 인터리빙하는 것처럼 보이는 순차적 일관성(SC)입니다.

전역 메모리 순서는 프로그램 실행마다 다를 수 있으며 미리 알려지지 않을 수 있습니다. SC의 특징은 동시성 평면 (즉, 메모리 이미지)을 나타내는 주소-공간-시간 다이어그램의 수평 슬라이스 세트입니다. 주어진 평면에서 모든 이벤트(또는 메모리 값)는 동시에 발생합니다. 모든 스레드가 동시에 어떤 메모리 값을 사용하는지 동의하는 절대 시간 이라는 개념이 있습니다. SC에서는 매 순간 모든 스레드가 공유하는 메모리 이미지가 하나만 있습니다. 즉, 매 순간 모든 프로세서가 메모리 이미지(즉, 메모리의 전체 내용)에 동의합니다. 이것은 모든 스레드가 모든 메모리 위치에 대해 동일한 값 시퀀스를 볼 뿐만 아니라 모든 프로세서가 모든 변수 의 동일한 값 조합을 관찰한다는 것을 의미합니다. 이것은 모든 메모리 작업(모든 메모리 위치에서)이 모든 스레드에서 동일한 총 순서로 관찰된다는 것과 같습니다.

완화된 메모리 모델에서 각 스레드는 고유한 방식으로 주소-공간-시간을 분할합니다. 유일한 제한은 모든 스레드가 모든 개별 메모리 위치의 기록에 동의해야 하기 때문에 각 스레드의 슬라이스가 서로 교차하지 않아야 한다는 것입니다(물론 , 서로 다른 스레드의 조각이 서로 교차할 수 있고 교차할 수 있음). 그것을 쪼갤 수 있는 보편적인 방법은 없습니다(주소-공간-시간의 특권 분할 없음). 슬라이스가 평면(또는 선형)일 필요는 없습니다. 그것들은 곡선일 수 있고 이것이 쓰레드가 쓰레드가 쓰레드가 쓰인 순서와 다른 값을 읽게 만들 수 있습니다. 다른 메모리 위치의 히스토리는 특정 쓰레드에서 볼 때 서로에 대해 임의로 미끄러질 수 있습니다(또는 늘어나게 될 수 있음) . 각 스레드는 동시에 발생하는 이벤트(또는 이에 상응하는 메모리 값)에 대해 서로 다른 의미를 갖습니다. 한 스레드와 동시에 발생하는 일련의 이벤트(또는 메모리 값)는 다른 스레드와 동시에 발생하지 않습니다. 따라서 완화된 메모리 모델에서 모든 스레드는 여전히 각 메모리 위치에 대해 동일한 기록(즉, 값 시퀀스)을 관찰합니다. 그러나 그들은 다른 메모리 이미지(즉, 모든 메모리 위치의 값 조합)를 관찰할 수 있습니다. 같은 쓰레드가 두 개의 서로 다른 메모리 위치를 순차적으로 기록하더라도, 새로 기록된 두 값은 다른 쓰레드에 의해 다른 순서로 관찰될 수 있다.

[위키피디아에서 가져온 사진] 위키피디아의 사진

아인슈타인의 특수 상대성 이론에 익숙한 독자는 내가 암시하는 내용을 알아차릴 것입니다. Minkowski의 말을 메모리 모델 영역으로 번역하면 주소 공간과 시간은 주소 공간 시간의 그림자입니다. 이 경우, 각 관찰자(즉, 스레드)는 이벤트의 그림자(즉, 메모리 저장/로드)를 자신의 세계선(즉, 시간 축)과 자신의 동시성 평면(주소-공간 축)에 투영합니다. . C++11 메모리 모델의 스레드는 특수 상대성 이론에서 서로에 대해 움직이는 관찰자에 해당합니다. 순차적 일관성은 갈릴레이 시공간에 해당합니다(즉, 모든 관찰자는 사건의 하나의 절대적 순서와 동시성에 대한 전지구적 감각에 동의합니다).

기억 모델과 특수 상대성 이론의 유사성은 둘 다 종종 인과 관계 집합이라고 하는 부분적으로 정렬된 사건 집합을 정의한다는 사실에서 비롯됩니다. 일부 이벤트(예: 메모리 저장소)는 다른 이벤트에 영향을 줄 수 있지만 영향을 받지는 않습니다. C++11 스레드(또는 물리학의 관찰자)는 이벤트의 체인(즉, 완전히 정렬된 집합)에 불과합니다(예: 메모리 로드 및 가능한 다른 주소 저장).

상대성 이론에서 모든 관찰자가 동의하는 유일한 시간적 순서는 "시간적" 사건(즉, 더 느리게 진행하는 입자에 의해 원칙적으로 연결될 수 있는 사건) 사이의 순서이기 때문에 일부 질서는 부분적으로 정렬된 사건의 겉보기에 혼란스러운 그림으로 복원됩니다. 진공에서 빛의 속도보다). 시간과 관련된 이벤트만 변함없이 정렬됩니다. 시간 물리학, Craig Callender .

C++11 메모리 모델에서 유사한 메커니즘(획득-해제 일관성 모델)이 이러한 로컬 인과 관계 를 설정하는 데 사용됩니다.

메모리 일관성에 대한 정의와 SC 포기에 대한 동기를 제공하기 위해 "메모리 일관성 및 캐시 일관성에 대한 입문서"를 인용하겠습니다.

공유 메모리 시스템의 경우 메모리 일관성 모델은 메모리 시스템의 구조적으로 볼 수 있는 동작을 정의합니다. 단일 프로세서 코어에 대한 정확성 기준은 " 하나의 올바른 결과 "와 " 많은 잘못된 대안 " 사이에서 동작을 분할합니다. 이는 프로세서의 아키텍처에서 스레드 실행이 순서가 잘못된 코어에서도 주어진 입력 상태를 잘 정의된 단일 출력 상태로 변환하도록 요구하기 때문입니다. 그러나 공유 메모리 일관성 모델은 여러 스레드의 로드 및 저장과 관련이 있으며 일반적으로 많은(더 많은) 잘못된 실행을 허용하지 않고 많은 올바른 실행을 허용합니다. 여러 올바른 실행의 가능성은 여러 스레드가 동시에 실행되도록 허용하는 ISA 때문이며, 종종 다른 스레드의 명령을 합법적으로 인터리빙할 수 있습니다.

느슨 하거나 약한 메모리 일관성 모델은 강력한 모델에서 대부분의 메모리 순서가 필요하지 않다는 사실에 동기를 부여합니다. 스레드가 10개의 데이터 항목을 업데이트한 다음 동기화 플래그를 업데이트하는 경우 프로그래머는 일반적으로 데이터 항목이 서로에 대해 순서대로 업데이트되는지 여부를 신경 쓰지 않고 플래그가 업데이트되기 전에 모든 데이터 항목이 업데이트된다는 점만 고려합니다(일반적으로 FENCE 명령을 사용하여 구현됨 ). 완화된 모델은 이러한 증가된 주문 유연성을 포착하고 프로그래머가 SC의 더 높은 성능과 정확성을 모두 얻기 위해 "필요로 하는" 주문만 보존하려고 합니다. 예를 들어, 특정 아키텍처에서 FIFO 쓰기 버퍼는 캐시에 결과를 쓰기 전에 커밋된(폐기된) 저장소의 결과를 유지하기 위해 각 코어에서 사용됩니다. 이 최적화는 성능을 향상시키지만 SC를 위반합니다. 쓰기 버퍼는 매장 누락 서비스의 대기 시간을 숨깁니다. 상점은 일반적이기 때문에 대부분의 상점에서 정체를 피할 수 있다는 것이 중요한 이점입니다. 단일 코어 프로세서의 경우 주소 A에 대한 로드가 A에 대한 하나 이상의 저장소가 쓰기 버퍼에 있는 경우에도 A에 대한 가장 최근 저장소의 값을 반환하도록 하여 쓰기 버퍼를 구조적으로 보이지 않게 만들 수 있습니다. 이것은 일반적으로 A에 대한 가장 최근 저장소의 값을 A에서 로드로 우회하여 수행됩니다. 여기서 "가장 최근"은 프로그램 순서에 따라 결정됩니다. 또는 A에 대한 저장소가 쓰기 버퍼에 있는 경우 A의 로드를 지연시킵니다. . 여러 코어를 사용하는 경우 각 코어에는 자체 바이패스 쓰기 버퍼가 있습니다. 쓰기 버퍼가 없으면 하드웨어는 SC이지만 쓰기 버퍼가 있으면 그렇지 않으므로 쓰기 버퍼를 멀티코어 프로세서에서 구조적으로 볼 수 있습니다.

코어에 FIFO가 아닌 쓰기 버퍼가 있는 경우 매장에서 매장이 입력된 순서와 다른 순서로 출발하도록 하는 경우 매장-매장 재정렬이 발생할 수 있습니다. 이는 첫 번째 저장소가 두 번째 히트 동안 캐시에서 누락되거나 두 번째 저장소가 이전 저장소(즉, 첫 번째 저장소 이전)와 통합될 수 있는 경우 발생할 수 있습니다. 로드 로드 재정렬은 프로그램 순서를 벗어나 명령을 실행하는 동적으로 예약된 코어에서도 발생할 수 있습니다. 이는 다른 코어에서 저장소를 재정렬하는 것과 동일하게 작동할 수 있습니다(두 스레드 간의 인터리빙 예제를 생각해낼 수 있습니까?). 이전 로드를 나중 저장소로 재정렬하면(로드-저장소 재정렬) 보호하는 잠금을 해제한 후 값을 로드하는 등 여러 잘못된 동작이 발생할 수 있습니다(저장소가 잠금 해제 작업인 경우). 프로그램 순서대로 모든 명령어를 실행하는 코어가 있는 경우에도 일반적으로 구현되는 FIFO 쓰기 버퍼의 로컬 바이패스로 인해 저장 로드 재정렬이 발생할 수도 있습니다.

캐시 일관성과 메모리 일관성이 때때로 혼동되기 때문에 다음 인용문도 사용하는 것이 좋습니다.

일관성과 달리 캐시 일관성 은 소프트웨어에 표시되지도 않고 필요하지도 않습니다. 일관성은 공유 메모리 시스템의 캐시를 단일 코어 시스템의 캐시처럼 기능적으로 보이지 않게 만들려고 합니다. 정확한 일관성은 프로그래머가 로드 및 저장 결과를 분석하여 시스템에 캐시가 있는지 여부와 위치를 결정할 수 없도록 합니다. 정확한 일관성은 캐시가 새롭거나 다른 기능적 동작을 활성화하지 않도록 보장하기 때문입니다(프로그래머는 여전히 타이밍 정보를 사용하여 가능한 캐시 구조를 추론할 수 있음). 캐시 일관성 프로토콜의 주요 목적은 모든 메모리 위치에 대해 SWMR(Single-Writer-Multiple-Readers)을 불변으로 유지하는 것입니다. 일관성과 일관성의 중요한 차이점은 일관성 이 메모리 위치별로 지정되는 반면 일관성은 모든 메모리 위치에 대해 지정된다는 것입니다.

우리의 정신적 그림을 계속해서 살펴보면 SWMR 불변량은 한 위치에 최대 하나의 입자가 있지만 모든 위치에 무제한의 관찰자가 있을 수 있다는 물리적 요구 사항에 해당합니다.


Ahmed Nassar

이것은 이제 몇 년 된 질문이지만 매우 인기가 있으므로 C++11 메모리 모델에 대해 배울 수 있는 환상적인 리소스를 언급할 가치가 있습니다. 나는 이것을 또 다른 완전한 대답으로 만들기 위해 그의 연설을 요약하는 것이 의미가 없다고 생각하지만 이것이 실제로 표준을 작성한 사람이라는 점을 감안할 때 연설을 볼 가치가 있다고 생각합니다.

Herb Sutter는 "atomic<> Weapons"라는 제목의 C++11 메모리 모델에 대해 3시간 동안 이야기했으며 Channel9 사이트 (1 및 2부)에서 사용할 수 있습니다. 이야기는 매우 기술적이고 다음 주제를 다룹니다.

  1. 최적화, 인종 및 메모리 모델
  2. 주문 – 무엇: 획득 및 해제
  3. 주문 – 방법: 뮤텍스, 원자 및/또는 울타리
  4. 컴파일러 및 하드웨어에 대한 기타 제한 사항
  5. 코드 생성 및 성능: x86/x64, IA64, POWER, ARM
  6. 릴렉스 아토믹스

강연에서는 API에 대해 자세히 설명하지 않고 추론, 배경, 내부 및 배후에서(POWER 및 ARM이 동기화된 로드를 효율적으로 지원하지 않기 때문에 표준에 느슨한 의미 체계가 추가되었다는 사실을 알고 계셨습니까?).


eran

이는 표준이 이제 다중 스레딩을 정의하고 다중 스레드의 컨텍스트에서 발생하는 일을 정의한다는 것을 의미합니다. 물론 사람들은 다양한 구현을 사용했지만 이는 모두 홈롤 string 클래스를 std::string

POSIX 스레드 또는 Windows 스레드에 대해 이야기할 때 이는 동시에 실행되는 하드웨어 기능이기 때문에 실제로 x86 스레드에 대해 이야기하는 것처럼 약간의 착각입니다. C++0x 메모리 모델은 사용자가 x86, ARM, MIPS 등 무엇을 사용하고 있는지에 상관없이 보장합니다.


Puppy

메모리 모델을 지정하지 않는 언어의 경우 프로세서 아키텍처에 지정된 언어 및 메모리 모델에 대한 코드를 작성합니다. 프로세서는 성능을 위해 메모리 액세스를 재정렬하도록 선택할 수 있습니다. 따라서 프로그램 에 데이터 경합이 있는 경우(데이터 경합은 여러 코어/하이퍼 스레드가 동일한 메모리에 동시에 액세스할 수 있는 경우임) 프로세서 메모리 모델에 대한 종속성으로 인해 프로그램이 교차 플랫폼이 아닙니다. 프로세서가 메모리 액세스를 재정렬하는 방법을 알아보려면 Intel 또는 AMD 소프트웨어 설명서를 참조하십시오.

매우 중요한 것은 잠금(잠금이 있는 동시성 의미 체계)이 일반적으로 교차 플랫폼 방식으로 구현된다는 것입니다... 따라서 데이터 경합이 없는 다중 스레드 프로그램에서 표준 잠금을 사용하는 경우 교차 플랫폼 메모리 모델에 대해 걱정할 필요가 없습니다. .

흥미롭게도 C++용 Microsoft 컴파일러는 C++에서 메모리 모델 부족을 처리하기 위한 C++ 확장인 volatile에 대한 획득/해제 의미 체계를 가지고 있습니다. http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80).aspx . 그러나 Windows가 x86/x64에서만 실행된다는 점을 감안할 때 별 의미는 없습니다(Intel 및 AMD 메모리 모델은 언어로 의미 체계를 획득/해제하는 것을 쉽고 효율적으로 구현함).


ritesh

뮤텍스를 사용하여 모든 데이터를 보호하는 경우 걱정할 필요가 없습니다. 뮤텍스는 항상 충분한 순서와 가시성을 보장합니다.

이제 원자 또는 잠금 해제 알고리즘을 사용했다면 메모리 모델에 대해 생각해야 합니다. 메모리 모델은 원자가 주문 및 가시성 보장을 제공하는 시기를 정확하게 설명하고 손으로 코딩된 보장을 위한 휴대용 펜스를 제공합니다.

이전에는 컴파일러 내장 또는 일부 상위 수준 라이브러리를 사용하여 원자성을 수행했습니다. 펜스는 CPU별 명령(메모리 장벽)을 사용하여 수행되었을 것입니다.


ninjalj

위의 답변은 C++ 메모리 모델의 가장 기본적인 측면을 파악합니다. 실제로, 대부분의 std::atomic<> 사용은 최소한 프로그래머가 과도하게 최적화할 때까지 "그냥 작동"합니다(예: 너무 많은 것을 완화하려고 시도함으로써).

실수가 여전히 흔한 한 곳이 있습니다. 바로 시퀀스 잠금 입니다. https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf에 문제에 대한 훌륭하고 읽기 쉬운 토론이 있습니다. 독자가 잠금 단어에 쓰는 것을 피하기 때문에 시퀀스 잠금이 매력적입니다. 다음 코드는 위 기술 보고서의 그림 1을 기반으로 하며 C++에서 시퀀스 잠금을 구현할 때의 문제를 강조합니다.

 atomic<uint64_t> seq; // seqlock representation int data1, data2; // this data will be protected by seq T reader() { int r1, r2; unsigned seq0, seq1; while (true) { seq0 = seq; r1 = data1; // INCORRECT! Data Race! r2 = data2; // INCORRECT! seq1 = seq; // if the lock didn't change while I was reading, and // the lock wasn't held while I was reading, then my // reads should be valid if (seq0 == seq1 && !(seq0 & 1)) break; } use(r1, r2); } void writer(int new_data1, int new_data2) { unsigned seq0 = seq; while (true) { if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1)) break; // atomically moving the lock from even to odd is an acquire } data1 = new_data1; data2 = new_data2; seq = seq0 + 2; // release the lock by increasing its value to even }

처음에는 직관적이지 않지만 data1data2atomic<> 이어야 합니다. 그것들이 원자적이지 않다면 writer() ) 쓰여지는 것과 똑같은 시간에 reader() C++ 메모리 모델에 따르면 이는 reader() 실제로 데이터를 사용하지 않더라도 경쟁입니다. 또한 원자성이 아닌 경우 컴파일러는 레지스터에 있는 각 값의 첫 번째 읽기를 캐시할 수 있습니다. 분명히 당신은 그것을 원하지 않을 것입니다 ... 당신은 reader() while 루프의 각 반복에서 다시 읽고 싶습니다.

atomic<> memory_order_relaxed 접근하는 것만으로는 충분하지 않습니다. 그 이유는 ( reader() ) seq의 읽기가 의미를 획득하기 때문입니다. 간단히 말해서 X와 Y가 메모리 액세스이고 X가 Y보다 앞서고 X가 획득 또는 해제가 아니며 Y가 획득이면 컴파일러는 X보다 먼저 Y를 재정렬할 수 있습니다. Y가 seq의 두 번째 읽기이고 X 데이터 읽기였으므로 이러한 재정렬은 잠금 구현을 깨뜨릴 것입니다.

이 논문은 몇 가지 해결책을 제시합니다. 오늘날 최고의 성능을 보이는 것은 아마도 seqlock의 두 번째 읽기 전에 memory_order_relaxed 와 함께 atomic_thread_fence 논문에서는 그림 6을 보여줍니다. 여기까지 코드를 재현한 것은 아닙니다. 여기까지 읽은 사람이라면 반드시 논문을 읽어야 하기 때문입니다. 이 게시물보다 더 정확하고 완벽합니다.

data 변수를 원자로 만드는 것이 부자연스러울 수 있다는 것입니다. 코드에서 할 수 없다면 비원자에서 원자로 캐스팅하는 것은 기본 유형에서만 합법적이기 때문에 매우 주의해야 합니다. C++20은 이 문제를 더 쉽게 해결할 수 있도록 atomic_ref<>

요약하자면: C++ 메모리 모델을 이해하고 있다고 생각하더라도 고유한 시퀀스 잠금을 실행하기 전에 매우 주의해야 합니다.


Mike Spear

C 및 C++는 잘 구성된 프로그램의 실행 추적으로 정의되었습니다.

이제 절반은 프로그램의 실행 추적에 의해 정의되고 절반은 동기화 개체에 대한 많은 순서에 의해 사후 정의됩니다.

이러한 언어 정의는 이 두 가지 접근 방식을 혼합하는 논리적 방법이 전혀 없기 때문에 의미가 없습니다. 특히 뮤텍스 또는 원자 변수의 파괴는 잘 정의되어 있지 않습니다.


curiousguy

출처 : http:www.stackoverflow.com/questions/6319146/c11-introduced-a-standardized-memory-model-what-does-it-mean-and-how-is-it-g

반응형