C ++ 11에서는 일반적으로 volatile
스레딩에 사용하지 않으며 MMIO에만 사용 합니다.
그러나 TL : DR mo_relaxed
은 일관성있는 캐시 (즉, 모든 것)가있는 하드웨어에서 원자처럼 "작동"합니다 . vars를 레지스터에 유지하는 컴파일러를 중지하는 것으로 충분합니다. atomic
원 자성 또는 스레드 간 가시성을 생성하기 위해 메모리 장벽이 필요하지 않으며, 현재 스레드가 다른 스레드에 대한이 스레드의 액세스 사이에서 순서를 만들기 위해 작업 전후에 대기하도록하기 위해서만 필요합니다. mo_relaxed
장벽,로드, 저장 또는 RMW가 필요하지 않습니다.
C ++ 11 이전의 나쁜 시절volatile
에 장벽을 가진 인라인 -asm을 가진 롤-자체 원자 의 경우 , 일 을 처리하는 유일한 방법이었습니다 . 그러나 구현 방식에 대한 많은 가정에 의존했으며 표준에 의해 보장되지 않았습니다.std::atomic
volatile
예를 들어, Linux 커널은 여전히 고유 한 수동 롤 원자를 사용 volatile
하지만 몇 가지 특정 C 구현 (GNU C, clang 및 ICC) 만 지원합니다. 부분적으로는 GNU C 확장과 인라인 asm 구문 및 의미론 때문이지만 컴파일러 작동 방식에 대한 몇 가지 가정에도 의존하기 때문입니다.
새로운 프로젝트에는 거의 항상 잘못된 선택입니다. std::atomic
(with std::memory_order_relaxed
)를 사용 하여 컴파일러와 동일한 효율적인 기계 코드를 생성 할 수 있습니다 volatile
. std::atomic
와 mo_relaxed
쓸모 없게 volatile
스레딩을 위해. (어쩌면 일부 컴파일러에서 누락 된 최적화 버그를 atomic<double>
해결할 수는 없습니다)
std::atomic
메인 스트림 컴파일러 (gcc 및 clang 등) 의 내부 구현은 내부적으로 만 사용되는 것이 아닙니다volatile
. 컴파일러는 원자로드, 저장 및 RMW 내장 기능을 직접 노출합니다. (예 : "일반"객체에서 작동하는 GNU C __atomic
내장 .)
휘발성은 실제로 사용할 수 있습니다 (그러나하지 마십시오)
즉, CPU가 작동하는 방식 (일관된 캐시) 및 작동 방식에 대한 공유 가정으로 인해 실제 CPU에서 기존의 모든 C ++ 구현에 volatile
대한 exit_now
플래그 와 같은 것들에 실제로 사용할 수 volatile
있습니다. 그러나 그다지 많지 않으며 권장 되지 않습니다. 이 답변의 목적은 기존 CPU 및 C ++ 구현이 실제로 어떻게 작동하는지 설명하는 것입니다. 관심이 없다면, 스레딩을 위해 std::atomic
mo_relaxed가 더 이상 사용되지 않는다는 것만 알면 volatile
됩니다.
(ISO C ++ 표준은 매우 모호합니다. volatile
액세스는 C ++ 추상 머신의 규칙에 따라 엄격하게 평가되어야하며 최적화되지는 않습니다. 실제 구현에서는 머신의 메모리 주소 공간을 사용하여 C ++ 주소 공간을 모델링합니다. 이는 volatile
메모리에서 객체 표현에 액세스하기 위해 명령어를로드 / 저장하기 위해 읽기 및 할당이 컴파일되어야 함을 의미 합니다.)
다른 대답에서 알 수 있듯이 exit_now
플래그는 동기화가 필요없는 단순한 스레드 간 통신 사례입니다 . 배열 내용이 준비되었거나 그와 비슷한 것을 게시하지 않습니다. 다른 스레드에서 최적화되지 않은로드로 인해 즉시 발견되는 상점입니다.
// global
bool exit_now = false;
// in one thread
while (!exit_now) { do_stuff; }
// in another thread, or signal handler in this thread
exit_now = true;
휘발성 또는 원자가 없으면, 데이터 규칙 UB가없는 as-if 규칙과 가정은 컴파일러가 무한 루프에 들어가기 전에 (또는 그렇지 않은 경우) 플래그를 한 번만 검사하는 asm으로 최적화 할 수 있도록합니다 . 이것은 실제 컴파일러에서 실제로 일어나는 일입니다. (그리고 do_stuff
루프가 종료되지 않기 때문에 일반적으로 많은 부분을 최적화 하므로 루프에 들어가면 결과를 사용했을 가능성이있는 코드에 도달 할 수 없습니다).
// Optimizing compilers transform the loop into asm like this
if (!exit_now) { // check once before entering loop
while(1) do_stuff; // infinite loop
}
멀티 스레딩 프로그램은 최적화 모드에서 멈췄지만 -O0에서 정상적으로 실행되는 것은 x86-64의 GCC에서 이것이 어떻게 발생하는지에 대한 예입니다 (GCC의 asm 출력 설명 포함). 또한 MCU 프로그래밍-C ++ O2 최적화는 루프 온 전자 장치에서 중단되고 SE 는 또 다른 예를 보여줍니다.
우리는 일반적으로 글로벌 변수를 포함하여 CSE와 호이스트가 루프에서로드하는 공격적인 최적화를 원합니다 .
C ++ 11 이전volatile bool exit_now
에는 정상적인 C ++ 구현 에서이 작업을 의도 한대로 수행하는 한 가지 방법이었습니다 . 그러나 C ++ 11에서는 데이터 레이스 UB가 여전히 적용 volatile
되므로 HW 코 히어 런트 캐시를 가정하더라도 ISO 표준에 의해 실제로 모든 곳에서 작동 한다고 보장 되지는 않습니다 .
더 넓은 유형 volatile
의 경우 인열 부족을 보장하지 않습니다. bool
정상적인 구현에서는 문제가되지 않기 때문에 여기서의 차이점을 무시했습니다 . 그러나 이는 volatile
완화 원자와 동등한 것이 아니라 여전히 데이터 레이스 UB에 종속되는 이유의 일부이기도 합니다.
"의도 한대로"가 exit_now
스레드가 다른 스레드가 실제로 종료되기를 기다리는 것을 의미하지는 않습니다 . 또는 exit_now=true
이 스레드에서 이후 작업을 계속하기 전에 휘발성 저장소가 전체적으로 표시 될 때까지 기다립니다 . ( atomic<bool>
기본값 mo_seq_cst
은 나중에 seq_cst가 적어도로드되기 전에 대기하게합니다. 많은 ISA에서는 상점 이후에 완전한 장벽을 얻게됩니다).
C ++ 11은 동일하게 컴파일하는 비 UB 방식을 제공합니다.
용도한다 또는 "종료 지금"플래그 "계속 실행" std::atomic<bool> flag
과를mo_relaxed
사용
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
에서 얻을 수있는 것과 동일한 asm (비용이 많이 드는 장벽 지침 없음)을 제공합니다 volatile flag
.
티어링 없음은 물론, atomic
하나의 스레드에 저장하고 UB없이 다른 스레드에로드 할 수있는 기능을 제공하므로 컴파일러가 루프에서로드를 끌어 올릴 수 없습니다. (데이터-레이스 UB가 없다는 가정은 비 원자 비 휘발성 객체에 대한 적극적인 최적화를 가능하게하는 것입니다.)이 기능은 순수한로드 및 순수한 저장소와 atomic<T>
거의 동일 volatile
합니다.
atomic<T>
또한 +=
원자 RMW 작업을 수행하는 등의 작업을 수행합니다 (임시로드에 대한 원자로드보다 상당히 비싸고 별도의 원자 저장소를 운영합니다. 원자 RMW를 원하지 않는 경우 로컬 임시 코드를 사용하여 코드를 작성하십시오).
에서 기본 seq_cst
주문을 while(!flag)
받으면 주문 보증 wrt도 추가됩니다. 비 원자 접근 및 기타 원자 접근.
(이론적으로 ISO C ++ 표준 아토의 컴파일시 최적화를 배제하지 않는다. 그러나 실제로 컴파일러가 하지 않는 것을 확인되지 않을 것 제어 할 방법이 없기 때문입니다. 몇 가지 경우가 있습니다 곳도 volatile atomic<T>
없습니다 수도 컴파일러가 최적화하지 않은 경우 원자 최적화에 대한 충분한 제어가 가능하므로 이제는 컴파일러가 그렇지 않습니다. 컴파일러가 중복 std :: atomic 쓰기를 병합하지 않는 이유는 무엇입니까? wg21 / p0062 volatile atomic
는 현재 코드를 사용하여 원자.)
volatile
실제로 실제 CPU 에서이 작업을 수행하지만 여전히 사용하지는 않습니다.
약하게 정렬 된 메모리 모델 (비 x86)에서도 마찬가지입니다 . 그러나 실제로 사용을 사용하지 마십시오 atomic<T>
과 mo_relaxed
대신! 이 섹션의 요점은 실제 CPU가 작동하는 방식에 대한 오해를 해결하기위한 것이지 정당화하기위한 것이 아닙니다 volatile
. 잠금 코드를 작성하는 경우 성능에 관심이있을 수 있습니다. 캐시와 스레드 간 통신 비용을 이해하는 것은 일반적으로 좋은 성능을 위해 중요합니다.
실제 CPU에는 일관된 캐시 / 공유 메모리가 있습니다. 한 코어의 저장소가 전체적으로 표시되면 다른 코어는 오래된 값을 로드 할 수 없습니다 . ( 신화 프로그래머atomic<T>
는 seq_cst 메모리 순서를 가진 C ++ 과 동등한 Java 휘발성에 대해 이야기하는 CPU 캐시에 대해 믿습니다 .)
load 라고 말할 때 메모리에 액세스하는 asm 명령어를 의미합니다. 이것이 volatile
액세스가 보장 하는 것이며 비 원자 / 비 휘발성 C ++ 변수의 lvalue-to-rvalue 변환과 동일 하지 않습니다 . (예 : local_tmp = flag
또는 while(!flag)
).
패배해야 할 유일한 것은 첫 번째 검사 후에 전혀 다시로드되지 않는 컴파일 타임 최적화입니다. 순서없이 각 반복에 대한로드 + 확인이면 충분합니다. 이 스레드와 기본 스레드간에 동기화가 없으면 정확히 상점이 발생한시기 또는로드 WRT의 순서에 대해 이야기하는 것은 의미가 없습니다. 루프의 다른 작업. 이 글타래 가 보일 때만 중요합니다. exit_now 플래그가 설정되면 종료됩니다. 일반적인 x86 Xeon의 코어 간 지연 시간 은 별도의 물리적 코어 간 40ns와 같을 수 있습니다 .
이론적으로 : 코 히어 런트 캐시가없는 하드웨어의 C ++ 스레드
프로그래머가 소스 코드에서 명시 적 플러시를 수행하지 않고도 순수한 ISO C ++로 원격으로 효율적 일 수있는 방법을 보지 못했습니다.
이론적으로는 그렇지 않은 머신에서 C ++ 구현을 가질 수 있으며 다른 코어의 다른 스레드에서 볼 수 있도록 컴파일러 생성 명시 적 플러시가 필요 합니다. (또는 읽기를 위해 아마도 어리석은 사본을 사용하지 마십시오). C ++ 표준은 이것이 불가능하지는 않지만 C ++의 메모리 모델은 일관된 공유 메모리 시스템에서 효율적으로 설계되었습니다. 예를 들어 C ++ 표준은 "읽기-읽기 일관성", "쓰기-읽기 일관성"등에 대해서도 이야기합니다. 표준의 한 가지 참고 사항은 하드웨어에 대한 연결을 나타냅니다.
http://eel.is/c++draft/intro.races#19
[참고 : 앞의 네 가지 일관성 요구 사항은 두 연산이 모두 완화 된로드 인 경우에도 단일 연산에 대한 원자 연산의 컴파일러 재정렬을 효과적으로 허용하지 않습니다. 이를 통해 대부분의 하드웨어가 제공하는 캐시 일관성 보장을 C ++ 원자 작업에 효과적으로 사용할 수 있습니다. — 끝 참고]
release
상점이 자신을 플러시하는 몇 가지 메커니즘 과 선택된 주소 범위는 없습니다. 획득로드가이 릴리스 저장소를 본 경우 다른 스레드가 무엇을 읽고 싶을 지 모르기 때문에 모든 항목을 동기화해야합니다. 스레드간에 발생하는 관계를 설정하는 릴리스 순서는 쓰기 스레드에 의해 수행 된 이전의 비원 자적 작업이 읽기에 안전하다는 것을 보장합니다. 릴리스 저장소 이후에 추가 쓰기를하지 않는 한 ...) 또는 컴파일러가 일하기 정말 그 몇 캐시 라인이 필요 홍조를 증명하기 위해 스마트.
관련 : mov + mfence는 NUMA에서 안전합니까? 코 히어 런트 공유 메모리가없는 x86 시스템이 존재하지 않는 것에 대해 자세히 설명합니다. 관련 : 로드 / 스토어에 대한 자세한 내용은 ARM 에서 로드 및 저장 순서를 동일하게 지정하십시오.
이 있습니다 나는 비 간섭 공유 메모리 클러스터를 생각하지만, 그들은 단일 시스템 이미지 기계 아니에요. 각 일관성 도메인은 별도의 커널을 실행하므로 단일 C ++ 프로그램의 스레드를 실행할 수 없습니다. 대신 프로그램의 개별 인스턴스를 실행합니다 (각각 고유 한 주소 공간이 있음 : 한 인스턴스의 포인터는 다른 인스턴스에서는 유효하지 않습니다).
명시 적 플러시를 통해 서로 통신하려면 일반적으로 MPI 또는 기타 메시지 전달 API를 사용하여 프로그램에서 플러시해야하는 주소 범위를 지정하십시오.
실제 하드웨어는 std::thread
캐시 일관성 경계에서 실행되지 않습니다 .
물리적 주소 공간은 공유하지만 내부 공유 가능 캐시 도메인은 없는 비대칭 ARM 칩이 있습니다 . 따라서 일관성이 없습니다. (예 : 댓글 스레드 A8 코어 및 TI Sitara AM335x와 같은 Cortex-M3).
그러나 두 코어에서 스레드를 실행할 수있는 단일 시스템 이미지가 아닌 다른 코어가 해당 코어에서 실행됩니다. std::thread
코 히어 런트 캐시없이 CPU 코어 에서 스레드 를 실행하는 C ++ 구현을 알지 못합니다 .
특히 ARM의 경우 GCC와 clang은 모든 스레드가 동일한 내부 공유 가능 도메인에서 실행되는 것으로 가정하여 코드를 생성합니다. 실제로 ARMv7 ISA 설명서에는
이 아키텍처 (ARMv7)는 동일한 운영 체제 또는 하이퍼 바이저를 사용하는 모든 프로세서가 동일한 내부 공유 가능 공유 도메인에있을 것으로 예상됩니다.
따라서 별도의 도메인 간 비 일관성 공유 메모리는 다른 커널에서 다른 프로세스 간 통신을 위해 공유 메모리 영역을 명시 적으로 시스템별로 사용하기위한 것입니다.
해당 컴파일러에서 (내부 공유 가능 장벽) 대 (시스템) 메모리 장벽을 사용하는 코드 생성에 대한 이 CoreCLR 토론을 참조하십시오 .dmb ish
dmb sy
다른 ISA에 대한 C ++ 구현 std::thread
이 비 코 히어 런트 캐시 가 있는 코어에서 실행되지 않는다는 주장을 합니다. 그러한 구현이 존재하지 않는다는 증거는 없지만 가능성은 거의 없습니다. 그렇게 작동하는 특정 이국적인 HW를 대상으로하지 않는 한 성능에 대한 생각은 모든 스레드간에 MESI와 유사한 캐시 일관성을 가정해야합니다. ( atomic<T>
그러나 정확성을 보장하는 방식으로 사용 하는 것이 좋습니다 !)
코 히어 런트 캐시로 간단하게
그러나 코 히어 런트 캐시가있는 멀티 코어 시스템에서 릴리스 저장소를 구현 한다는 것은 명시 적 플러시를 수행하지 않고이 스레드 저장소의 캐시에 커밋을 주문하는 것을 의미합니다. ( https://preshing.com/20120913/acquire-and-release-semantics/ 및 https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). 그리고 획득로드는 다른 코어의 캐시에 대한 액세스 권한을 주문하는 것을 의미합니다.
메모리 배리어 명령은 저장 버퍼가 비워 질 때까지 현재 스레드의로드 및 / 또는 저장을 막습니다. 항상 가능한 한 빨리 발생합니다. ( 메모리 장벽이 캐시 일관성이 완료되었는지 확인합니까? 이 오해를 해결합니다). 따라서 주문이 필요하지 않은 경우 다른 스레드에서 즉시 가시성을 확보하는 mo_relaxed
것이 좋습니다. (그렇지만 그렇게 volatile
하지 마십시오.)
프로세서에 대한 C / C ++ 11 맵핑
도 참조하십시오.
재미있는 사실 : x86 메모리 모델은 기본적으로 seq-cst와 저장 버퍼 (저장소 전달)가 있기 때문에 x86에서 모든 asm 저장은 릴리스 저장소입니다.
반 관련 re : 저장 버퍼, 글로벌 가시성 및 일관성 : C ++ 11은 거의 보장하지 않습니다. 대부분의 실제 ISA (PowerPC 제외)는 모든 스레드가 두 개의 다른 스레드에 의해 두 개의 저장소가 나타나는 순서에 동의 할 수 있음을 보장합니다. 공식적인 컴퓨터 아키텍처 메모리 모델 용어에서는 "다중 복사 원자"입니다.
또 다른 오해는 다른 코어가 매장 을 전혀 볼 수 있도록 매장 버퍼를 비우려면 메모리 펜스 asm 명령이 필요하다는 것 입니다. 실제로 저장소 버퍼는 항상 가능한 빨리 자신을 비우려고합니다 (L1d 캐시에 커밋). 그렇지 않으면 실행이 가득 차고 중단됩니다. 전체 배리어 / 펜스가하는 것은 저장 버퍼가 비워 질 때까지 현재 스레드를 정지시키는 것이므로 이후의로드는 이전 저장 후 전체 순서로 나타납니다.
(86 강하게 ASM 메모리 모델 수단을 주문 사용자들은 volatile
가까이에 당신을주고 끝낼 수 x86에서 mo_acq_rel
여전히 일어날 수있는 비 원자 변수를 재정렬하는 컴파일 시간을 제외하고.하지만 대부분의 x86 이외의 메모리 모델을 너무 약하게-주문한 volatile
와 relaxed
같은 대해 수 있습니다 mo_relaxed
허용하면 약합니다 .)