멀티 스레딩에서 휘발성을 사용해야하는 경우


130

전역 변수에 액세스하는 두 개의 스레드가있는 경우 많은 자습서에서 컴파일러가 변수를 레지스터에서 캐싱하지 못하도록 변수를 휘발성으로 만들고 올바르게 업데이트되지 않는다고 말합니다. 그러나 공유 변수에 액세스하는 두 개의 스레드는 뮤텍스를 통한 보호를 요구하는 것이 아닙니다. 그러나이 경우 스레드 잠금과 뮤텍스 해제 사이에서 코드는 하나의 스레드 만 변수에 액세스 할 수있는 중요한 섹션에 있습니다.이 경우 변수는 휘발성이 필요하지 않습니까?

따라서 멀티 스레드 프로그램에서 휘발성의 사용 / 목적은 무엇입니까?


3
어떤 경우에는 뮤텍스의 보호를 원하지 않거나 필요하지 않습니다.
Stefan Mai

4
때로는 경쟁 조건을 갖는 것이 좋으며 때로는 그렇지 않습니다. 이 변수를 어떻게 사용하고 있습니까?
David Heffernan

3
@David : 레이스를하는 것이 "괜찮은"예를 들어주세요.
John Dibling

6
@ 존 여기 간다. 많은 작업을 처리하는 작업자 스레드가 있다고 가정하십시오. 작업자 스레드는 작업이 완료 될 때마다 카운터를 증가시킵니다. 마스터 스레드는 주기적으로이 카운터를 읽고 진행률에 대한 뉴스를 사용자에게 업데이트합니다. 카운터가 찢어지지 않도록 올바르게 정렬되어 있으면 액세스를 동기화 할 필요가 없습니다. 인종이 있지만 양성입니다.
David Heffernan

5
@John이 코드가 실행되는 하드웨어는 정렬 된 변수가 찢어지지 않도록 보장합니다. 독자가 읽을 때 작업자가 n을 n + 1로 업데이트하는 경우 독자는 n 또는 n + 1을 얻는 지 상관하지 않습니다. 진행률보고에만 사용되므로 중요한 결정은 없습니다.
David Heffernan

답변:


167

Short & quick answer : volatile플랫폼에 구애받지 않는 멀티 스레드 응용 프로그램 프로그래밍에는 거의 쓸모가 없습니다. 동기화를 제공하지 않으며 메모리 펜스를 생성하지 않으며 작업 실행 순서를 보장하지도 않습니다. 작업을 원 자성으로 만들지 않습니다. 코드가 마술처럼 스레드 안전하지는 않습니다. volatile모든 C ++에서 가장 오해 된 시설 일 수 있습니다. 참조 , 에 대한 자세한 내용은volatile

반면에, volatile그렇게 분명하지 않을 수도있는 용도가 있습니다. const컴파일러가 보호되지 않은 방식으로 일부 공유 리소스에 액세스 할 때 실수를 저지르는 위치를 컴파일러에 표시하는 데 사용하는 것과 같은 방식으로 많이 사용할 수 있습니다. 이 사용은 이 기사 에서 Alexandrescu에 의해 논의된다 . 그러나 이것은 기본적으로 종종 기여로 간주되고 정의되지 않은 동작을 유발할 수있는 방식으로 C ++ 유형 시스템을 사용합니다.

volatile특히 메모리 매핑 된 하드웨어, 신호 처리기 및 setjmp 기계 코드 명령어와 인터페이스 할 때 사용하기위한 것입니다. 따라서 volatile일반적인 응용 프로그램 수준 프로그래밍이 아닌 시스템 수준 프로그래밍에 직접 적용 할 수 있습니다.

2003 C ++ 표준에서는 volatile변수에 대해 Acquire 또는 Release 의미 를 적용 한다고 말하지 않습니다 . 실제로 표준은 멀티 스레딩의 모든 문제에 대해 완전히 침묵합니다. 그러나 특정 플랫폼은 volatile변수 에 획득 및 해제 시맨틱을 적용 합니다.

[C ++ 11 업데이트]

는 C ++ 11 표준 지금 하지 인정 메모리 모델과 lanuage에서 직접 멀티 스레딩, 그것은 플랫폼에 독립적 인 방식으로 처리하는 라이브러리 기능을 제공합니다. 그러나의 의미는 volatile여전히 바뀌지 않았습니다. volatile여전히 동기화 메커니즘이 아닙니다. Bjarne Stroustrup은 TCPPPL4E에서 다음과 같이 말합니다.

volatile하드웨어를 직접 다루는 저수준 코드를 제외하고는 사용하지 마십시오 .

volatile메모리 모델에서 특별한 의미가 있다고 가정하지 마십시오 . 그렇지 않습니다. 이후 언어와 마찬가지로 동기화 메커니즘이 아닙니다. 동기화하려면 atomic, a mutex또는을 사용하십시오 condition_variable.

[/ 최종 업데이트]

위의 모든 내용은 2003 표준 (현재 2011 표준)에 정의 된대로 C ++ 언어 자체에 적용됩니다. 그러나 일부 특정 플랫폼은 기능에 추가 기능 또는 제한을 추가합니다 volatile. 예를 들어, MSVC 2010에서 (적어도) 획득 및 릴리스 의미 volatile 변수 에 대한 특정 작업에 적용됩니다 . MSDN에서 :

최적화 할 때 컴파일러는 휘발성 객체에 대한 참조와 다른 전역 객체에 대한 참조 중 순서를 유지해야합니다. 특히,

휘발성 객체에 대한 쓰기 (휘발성 쓰기)에는 릴리스 시맨틱이 있습니다. 명령 시퀀스에서 휘발성 객체에 쓰기 전에 발생하는 전역 또는 정적 객체에 대한 참조는 컴파일 된 이진에서 휘발성 쓰기 전에 발생합니다.

휘발성 객체의 읽기 (휘발성 읽기)에는 획득 의미가 있습니다. 명령어 시퀀스에서 휘발성 메모리를 판독 한 후에 발생하는 전역 또는 정적 객체에 대한 참조는 컴파일 된 바이너리에서 휘발성 판독 후에 발생한다.

그러나 위의 링크를 따를 경우 시맨틱 획득 / 릴리스 시맨틱이 실제로 적용 되는지 여부에 대한 의견에 약간의 논쟁이 있다는 사실에 주목할 수 있습니다 .


19
내 일부는 답의 음조와 첫 번째 주석 때문에 이것을 낮추고 싶다. "휘발성은 쓸모가 없다"는 "수동 메모리 할당은 쓸모가 없다"와 유사하다. 멀티 스레드 프로그램을 작성할 수 없다면 스레딩 라이브러리를 구현 volatile했던 사람들의 어깨에 서기 때문 volatile입니다.
Ben Jackson

19
뭔가 믿음에 도전 @ 벤해서 그것을 생색하지 않습니다
데이비드 헤퍼 넌

38
@ 벤 : 아니, 무엇을 읽을 volatile실제로 않는 C ++로. @John이 말한 것은 이야기의 끝 이 맞습니다 . 응용 프로그램 코드 대 라이브러리 코드 또는 "일반"대 "신과 같은 전지구 적 프로그래머"와는 아무 관련이 없습니다. volatile스레드 간 동기화에는 불필요하고 쓸모가 없습니다. 스레딩 라이브러리는 다음과 같은 측면에서 구현할 수 없습니다 volatile. 어쨌든 플랫폼 별 세부 사항에 의존해야하며, 그에 의존 할 때 더 이상 필요하지 않습니다 volatile.
jalf

6
@jalf : "휘발성은 불필요하고 쓰레드 간 동기화에 쓸모가 없다"(여러분이 말한 것)는 "다중 스레드 프로그래밍에는 휘발성이 쓸모가 없습니다"(존이 답에서 말한 것)와 같지 않습니다. 100 % 정확하지만 John (부분적으로)에 동의하지 않습니다. 휘발성은 여전히 ​​멀티 스레드 프로그래밍에 사용될 수 있습니다 (매우 제한된 작업에 대해)

4
@GMan : 유용한 모든 것은 특정 요구 사항이나 조건 하에서 만 유용합니다. 휘발성은 엄격한 조건 세트 하에서 멀티 스레드 프로그래밍에 유용합니다 (일부 경우 대안보다 나은 경우도 있습니다). "이것을 무시하고 .."라고 말하지만 휘발성이 멀티 스레딩에 유용한 경우는 아무것도 무시하지 않습니다. 당신은 내가 결코 주장하지 않은 것을 구성했습니다. 예, 휘발성의 유용성은 제한적이지만 존재합니다. 그러나 동기화에 유용하지 않다는 데 모두 동의 할 수 있습니다.

31

(편집자 주 : C ++ 11 volatile에서는이 작업에 적합한 도구가 아니며 여전히 데이터 레이스 UB가 있습니다. UB없이로드 / 저장 std::atomic<bool>과 함께 사용 하여 std::memory_order_relaxed실제 구현에서는와 동일한 asm으로 컴파일됩니다 volatile. 더 자세하게 답변 하고 약하게 정렬 된 메모리는이 사용 사례에서 문제가 될 수 있다는 의견의 오해를 해결합니다. 모든 실제 CPU는 일관된 공유 메모리를 가지고 있으므로 실제 C ++ 구현 에서이volatile 작업 수행 할 수 있습니다. 하지마

의견에서 일부 토론은 완화 된 원자보다 강한 것이 필요한 다른 사용 사례에 대해 이야기하는 것 같습니다 . 이 답변은 이미 volatile주문하지 않은 것으로 나타났습니다 .)


휘발성은 때때로 다음과 같은 이유로 유용합니다 :이 코드 :

/* global */ bool flag = false;

while (!flag) {}

gcc는 다음과 같이 최적화했습니다.

if (!flag) { while (true) {} }

다른 스레드에서 플래그를 쓰면 분명히 올바르지 않습니다. 이 최적화가 없으면 동기화 메커니즘이 작동 할 수 있습니다 (다른 코드에 따라 일부 메모리 장벽이 필요할 수 있음). 1 생산자-1 소비자 시나리오에서는 뮤텍스가 필요하지 않습니다.

그렇지 않으면 volatile 키워드는 사용하기에는 너무 이상합니다. 휘발성 및 비 휘발성 액세스 모두에 대한 메모리 정렬을 보장하지 않으며 원자 연산을 제공하지 않습니다. .


4
내가 기억한다면 C ++ 0x 원자는 많은 사람들이 (잘못된) 휘발성 물질에 의해 올바르게 행동한다고 ​​생각합니다.
David Heffernan

13
volatile메모리 액세스가 재정렬되는 것을 막지 않습니다. volatile액세스는 서로에 대한 순서가되지 않습니다,하지만 그들은 제공하지 아니 비에 대한 재정렬에 대한 보증 volatile오브젝트를, 그래서, 그들은 기본적으로 플래그 쓸모뿐만 아니라입니다.
jalf

13
@Ben : 거꾸로 한 것 같아요. "휘발성은 쓸모가 없습니다"군중은 휘발성이 재정렬을 방지하지 못한다 는 단순한 사실에 의존합니다. 즉, 동기화에 전혀 쓸모가 없습니다. 다른 접근 방식도 마찬가지로 쓸모가 없을 수 있습니다 (앞서 언급했듯이 링크 타임 코드 최적화로 인해 컴파일러가 블랙 박스로 취급한다고 가정 한 코드를 컴파일러가 들여다 볼 수 있음) volatile.
jalf

15
@jalf : Arch Robinson (이 페이지의 다른 곳에서 링크), 10 번째 코멘트 ( "Spud")의 기사를 참조하십시오. 기본적으로 재정렬은 코드의 논리를 변경하지 않습니다. 게시 된 코드는 플래그를 사용하여 작업이 완료되었다는 신호가 아닌 작업을 취소하므로 코드 전후에 작업이 취소되는지 여부는 중요하지 않습니다 (예 : while (work_left) { do_piece_of_work(); if (cancel) break;}, 루프 내에서 취소가 재주문 된 경우, 논리는 여전히 유효합니다. 유사한 코드 조각이 있습니다 : 메인 스레드를 종료하려면 다른 스레드에 대한 플래그를 설정하지만 그렇지 않습니다.

15
... 다른 스레드가 플래그를 설정 한 후 합리적으로 발생하는 한 종료하기 전에 작업 루프를 몇 번 더 반복하면 중요합니다. 물론 이것은 내가 생각할 수있는 유일한 용도이며 오히려 틈새 시장입니다 (휘발성 변수에 쓰는 것이 다른 스레드에 변경 사항을 표시하지 않는 플랫폼에서는 작동하지 않을 수 있습니다. 적어도 x86 및 x86-64에서는 공장). 나는 확실히 아무 이유없이 실제로 그렇게하라고 조언하지 않을 것입니다. 나는 단지 "휘발성은 다중 스레드 코드에서 결코 유용하지 않습니다"와 같은 담요 진술이 100 % 정확하지 않다고 말하고 있습니다.

15

C ++ 11에서는 일반적으로 volatile스레딩에 사용하지 않으며 MMIO에만 사용 합니다.

그러나 TL : DR mo_relaxed은 일관성있는 캐시 (즉, 모든 것)가있는 하드웨어에서 원자처럼 "작동"합니다 . vars를 레지스터에 유지하는 컴파일러를 중지하는 것으로 충분합니다. atomic원 자성 또는 스레드 간 가시성을 생성하기 위해 메모리 장벽이 필요하지 않으며, 현재 스레드가 다른 스레드에 대한이 스레드의 액세스 사이에서 순서를 만들기 위해 작업 전후에 대기하도록하기 위해서만 필요합니다. mo_relaxed장벽,로드, 저장 또는 RMW가 필요하지 않습니다.

C ++ 11 이전의 나쁜 시절volatile 에 장벽을 가진 인라인 -asm을 가진 롤-자체 원자 의 경우 , 일 을 처리하는 유일한 방법이었습니다 . 그러나 구현 방식에 대한 많은 가정에 의존했으며 표준에 의해 보장되지 않았습니다.std::atomicvolatile

예를 들어, Linux 커널은 여전히 ​​고유 한 수동 롤 원자를 사용 volatile하지만 몇 가지 특정 C 구현 (GNU C, clang 및 ICC) 만 지원합니다. 부분적으로는 GNU C 확장과 인라인 asm 구문 및 의미론 때문이지만 컴파일러 작동 방식에 대한 몇 가지 가정에도 의존하기 때문입니다.

새로운 프로젝트에는 거의 항상 잘못된 선택입니다. std::atomic(with std::memory_order_relaxed)를 사용 하여 컴파일러와 동일한 효율적인 기계 코드를 생성 할 수 있습니다 volatile. std::atomicmo_relaxed쓸모 없게 volatile스레딩을 위해. (어쩌면 일부 컴파일러에서 누락 된 최적화 버그를 atomic<double>해결할 수는 없습니다)

std::atomic메인 스트림 컴파일러 (gcc 및 clang 등) 의 내부 구현은 내부적으로 만 사용되는 것이 아닙니다volatile . 컴파일러는 원자로드, 저장 및 RMW 내장 기능을 직접 노출합니다. (예 : "일반"객체에서 작동하는 GNU C __atomic내장 .)


휘발성은 실제로 사용할 수 있습니다 (그러나하지 마십시오)

즉, CPU가 작동하는 방식 (일관된 캐시) 및 작동 방식에 대한 공유 가정으로 인해 실제 CPU에서 기존의 모든 C ++ 구현에 volatile대한 exit_now플래그 와 같은 것들에 실제로 사용할 수 volatile있습니다. 그러나 그다지 많지 않으며 권장 되지 않습니다. 이 답변의 목적은 기존 CPU 및 C ++ 구현이 실제로 어떻게 작동하는지 설명하는 것입니다. 관심이 없다면, 스레딩을 위해 std::atomicmo_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 ishdmb 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 이외의 메모리 모델을 너무 약하게-주문한 volatilerelaxed같은 대해 수 있습니다 mo_relaxed허용하면 약합니다 .)


의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew

2
훌륭한 글쓰기. 이것이 바로 "단일 글로벌 공유 부울 플래그에 휘발성 대신 원자 사용"이라는 담요 선언 대신 내가 찾고있는 것입니다 ( 모든 사실을 제공함 ).
bernie

2
@bernie : 사용하지 않으면 캐시atomic 의 동일한 변수 대해 다른 값을 가진 다른 스레드로 이어질 수 있다는 반복적 인 주장에 좌절 한 후에 이것을 썼습니다 . / facepalm. 캐시에서, 아니오, CPU 레지스터에서 yes (비 원자 변수 포함); CPU는 코 히어 런트 캐시를 사용합니다. SO에 대한 다른 질문이 atomicCPU 작동 방식에 대한 오해를 퍼뜨렸다 는 설명으로 가득하지 않기를 바랍니다 . (이것은 성능상의 이유로 이해하는 데 유용하고 ISO C ++ 원자 규칙이 작성된 그대로의 이유를 설명하는데도 도움이됩니다.)
Peter Cordes

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

일단 휘발성이 쓸모 없다고 생각한 인터뷰어는 최적화가 어떤 문제도 일으키지 않을 것이며 별도의 캐시 라인을 가진 다른 코어를 언급하고 있다고 주장했습니다. 그러나이 코드는 g ++ (g ++ -O3 thread.cpp -lpthread)에서 -O3으로 컴파일 할 때 정의되지 않은 동작을 보여줍니다. 기본적으로 값이 while check 전에 설정되면 정상적으로 작동하고 그렇지 않으면 값을 가져 오는 것을 방해하지 않고 루프로 이동합니다 (실제로 다른 스레드에 의해 변경됨). 기본적으로 checkValue 값은 레지스터로 한 번만 가져오고 최고 수준의 최적화에서 다시 확인되지 않는다고 생각합니다. 가져 오기 전에 true로 설정하면 제대로 작동하고 그렇지 않으면 루프로 이동합니다. 틀렸다면 정정 해주세요.


4
이것은 무엇과 관련이 volatile있습니까? 예,이 코드는 UB이지만 UB volatile도 있습니다.
David Schwartz

-2

휘발성 및 잠금이 필요합니다.

volatile은 옵티 마이저에게 값이 비동기 적으로 변경 될 수 있음을 알려줍니다.

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

루프 주변에서 매번 플래그를 읽습니다.

최적화를 끄거나 모든 변수를 휘발성으로 만들면 프로그램은 동일하지만 느리게 동작합니다. 휘발성은 단지 '당신이 그것을 읽고 그 내용을 알고 있을지도 모른다는 것을 알고 있지만, 내가 읽었다면 읽으십시오.

잠금은 프로그램의 일부입니다. 그건 그렇고, 세마포어를 구현하는 경우 무엇보다도 휘발성이어야합니다. (어려워하지 마십시오. 어쩌면 약간의 어셈블러 또는 새로운 원자 재료가 필요할 수 있으며 이미 완료되었습니다.)


1
그러나 이것이 다른 응답의 동일한 예, 바쁜 대기 및 피해야 할 것이 아닌가? 이것이 유추 된 예라면, 유추되지 않은 실제 사례가 있습니까?
David Preston

7
@Chris : 바쁜 대기는 때때로 좋은 해결책입니다. 특히, 몇 번의 클럭 주기만 기다려야한다면 스레드를 중단시키는 훨씬 더 무거운 방법보다 오버 헤드가 훨씬 적습니다. 물론, 다른 의견에서 언급했듯이,이 예제와 같은 예제는 플래그에 대한 읽기 / 쓰기가 보호하는 코드와 관련하여 순서가 변경되지 않으며 그러한 보장이 제공되지 않는다고 가정하기 때문에 결함이 있습니다. , volatile이 경우에도 정말 유용하지 않습니다. 그러나 바쁜 대기는 때때로 유용한 기술입니다.
jalf

3
@richard 예와 아니오. 전반은 맞습니다. 그러나 이것은 단지 CPU와 컴파일러가 서로에 대해 휘발성 변수를 재정렬 할 수 없다는 것을 의미합니다. 휘발성 변수 A를 읽은 다음 휘발성 변수 B를 읽으면 컴파일러는 B 이전에 A를 읽도록 보장 된 (CPU 순서를 바꿔도) 코드를 생성해야하지만 모든 비 휘발성 변수 액세스에 대해 보장하지는 않습니다. . 휘발성 읽기 / 쓰기를 기준으로 순서를 바꿀 수 있습니다. 따라서 프로그램의 모든 변수를 휘발성으로 만들지 않으면 관심을 가지지 않습니다.
jalf

2
@ ctrl-alt-delor : volatile"재주문 없음"의 의미는 아닙니다. 상점이 프로그램 순서대로 (다른 스레드에 대해) 전역 적으로 표시 되기를 희망합니다 . 그게 당신에게 또는 무엇 atomic<T>을 제공합니다. 그러나 단지 당신에게 어떤 보장 제공 컴파일 시간의 재정렬을 각 액세스 프로그램 순서에서 ASM에 나타납니다. 장치 드라이버에 유용합니다. 현재 코어 / 스레드에서 인터럽트 처리기, 디버거 또는 신호 처리기와의 상호 작용에 유용하지만 다른 코어와의 상호 작용에는 유용하지 않습니다. memory_order_releaseseq_cstvolatile
Peter Cordes

1
volatile실제로는 keep_running당신이하고있는 것처럼 플래그 를 확인하기에 충분합니다 . 실제 CPU에는 항상 수동 플러시가 필요없는 코 히어 런트 캐시가 있습니다. 그러나 추천 할 이유가 없다 volatile이상 atomic<T>으로는 mo_relaxed; 당신은 같은 asm을 얻을 것이다.
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.