C ++ volatile 키워드가 메모리 울타리를 도입합니까?


85

volatile컴파일러에게 값이 변경 될 수 있음 을 알리는 것을 이해 하지만이 기능을 수행하려면 컴파일러가 작동하도록 메모리 펜스를 도입해야합니까?

내 이해에서 휘발성 개체에 대한 작업 순서는 재정렬 할 수 없으며 보존해야합니다. 이것은 일부 메모리 펜스가 필요하며 실제로이 문제를 해결할 방법이 없음을 의미하는 것 같습니다. 이 말이 맞습니까?


이 관련 질문에 흥미로운 토론 이 있습니다.

Jonathan Wakely는 다음과 같이 씁니다 .

... 별개의 휘발성 변수에 대한 액세스는 별도의 전체 표현식에서 발생하는 한 컴파일러에서 재정렬 할 수 없습니다. 휘발성은 스레드 안전성에 쓸모가 없지만 그가 제공하는 이유가 아닙니다. 컴파일러가 휘발성 객체에 대한 액세스를 재정렬 할 수 있기 때문이 아니라 CPU가이를 재정렬 할 수 있기 때문입니다. 원자 적 연산과 메모리 장벽으로 인해 컴파일러와 CPU의 순서가 변경되지 않습니다.

되는 데이비드 슈워츠는 응답 코멘트에 :

... C ++ 표준의 관점에서 보면 컴파일러가 무언가를 수행하는 것과 컴파일러가 하드웨어가 무언가를 수행하도록하는 명령어를 방출하는 것 사이에는 차이가 없습니다. CPU가 휘발성에 대한 액세스를 재정렬 할 수 있다면 표준은 순서를 유지할 것을 요구하지 않습니다. ...

... C ++ 표준은 재정렬이 무엇인지 구분하지 않습니다. 그리고 CPU가 관찰 가능한 효과없이 순서를 변경할 수 있다고 주장 할 수는 없습니다. C ++ 표준은 순서를 관찰 가능한 것으로 정의합니다. 컴파일러는 플랫폼이 표준에서 요구하는 작업을 수행하도록하는 코드를 생성하는 경우 플랫폼에서 C ++ 표준을 준수합니다. 표준이 휘발성 물질에 대한 액세스를 재정렬하지 않도록 요구하는 경우 재정렬하는 플랫폼은 규정을 준수하지 않습니다. ...

내 요점은 C ++ 표준이 컴파일러가 개별 휘발성에 대한 액세스 순서를 재정렬하는 것을 금지하는 경우 이러한 액세스 순서가 프로그램의 관찰 가능한 동작의 일부라는 이론에 따라 컴파일러가 CPU가 수행하는 것을 금지하는 코드를 내보내도록 요구한다는 것입니다. 그래서. 표준은 컴파일러가하는 일과 컴파일러의 생성 코드가 CPU가하는 일을 구별하지 않습니다.

다음 중 두 가지 질문이 생성됩니다. 둘 중 하나가 "맞습니까?" 실제 구현은 실제로 무엇을합니까?


9
이는 대부분 컴파일러가 해당 변수를 레지스터에 보관해서는 안된다는 것을 의미합니다. 소스 코드의 모든 할당 및 읽기는 바이너리 코드의 메모리 액세스와 일치해야합니다.
Basile Starynkevitch 2010


1
요점은 값이 내부 레지스터에 저장되면 메모리 펜스가 효과가 없다는 것입니다. 동시 상황에서 여전히 다른 보호 조치를 취해야한다고 생각합니다.
Galik

내가 아는 한, 휘발성은 하드웨어 (종종 마이크로 컨트롤러와 함께 사용)에 의해 변경 될 수있는 변수에 사용됩니다. 단순히 변수를 읽는 것이 다른 순서로 수행 될 수없고 최적화 될 수 없음을 의미합니다. 그래도 C이지만 ++에서는 동일해야합니다.
Mast

1
@Mast 나는 아직 volatile변수 읽기가 CPU 캐시에 의해 최적화되는 것을 방지하는 컴파일러를 보지 못했습니다 . 이 모든 컴파일러가 부적합하거나 표준이 의미하는 바를 의미하지 않습니다. (표준 컴파일러가 수행 CPU를 만드는 것 컴파일러가하는 것과 구분하지 않습니다 그것은 발광 코드 컴파일러의 일이다, 그 때 실행 표준을 준수합니다..)
데이비드 슈워츠

답변:


58

무엇을하는지 설명하는 대신 volatile을 사용해야하는시기를 설명하겠습니다 volatile.

  • 시그널 핸들러 안에있을 때. volatile변수에 쓰는 것은 표준이 신호 처리기 내에서 할 수있는 유일한 작업이기 때문입니다. C ++ 11부터는 std::atomic그 목적으로 사용할 수 있지만 원자가 잠금이없는 경우에만 사용할 수 있습니다 .
  • setjmp 인텔에 따라 다룰 때 .
  • 하드웨어를 직접 처리 할 때 컴파일러가 읽기 또는 쓰기를 최적화하지 않도록해야합니다.

예를 들면 :

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

volatile지정자가 없으면 컴파일러는 루프를 완전히 최적화 할 수 있습니다. volatile지정은 이후 2가 같은 값을 반환 읽는 가정하지 않을 수 컴파일러를 알려줍니다.

volatile스레드와는 아무 상관이있다. 위의 예제는 *foo관련된 취득 작업이 없기 때문에 쓰는 다른 스레드가있는 경우 작동하지 않습니다 .

다른 모든 경우에는 volatileC ++ 11 이전 컴파일러 및 컴파일러 확장 (예 /volatile:ms: X86 / I64에서 기본적으로 활성화되는 msvc의 스위치)을 처리 할 때를 제외하고는을 (를) 이식 할 수없는 것으로 간주하고 더 이상 코드 검토를 통과하지 않아야 합니다.


5
"두 번의 후속 읽기가 동일한 값을 반환한다고 가정하지 못할 수 있음"보다 더 엄격합니다. 한 번만 읽거나 값을 버리더라도 읽기는 완료되어야합니다.
philipxy 2014 년

1
신호 처리기에서의 사용 setjmp은 표준이 만드는 두 가지 보증입니다. 반면 에 적어도 처음에는 메모리 매핑 된 IO를 지원 하는 것이 목적 이었습니다. 일부 프로세서에서는 펜스 또는 membar가 필요할 수 있습니다.
제임스 간제

@philipxy 아무도 "읽기"가 무엇을 의미하는지 모른다는 것을 제외하고는. 예를 들어, 아무도 메모리에서 실제 읽기를 수행해야한다고 생각하지 않습니다 volatile. 액세스시 CPU 캐시를 우회하려는 컴파일러는 없습니다 .
David Schwartz

@JamesKanze : 그렇지 않습니다. 재 신호 처리기 표준은 신호 처리 중에 휘발성 std :: sig_atomic_t 및 잠금이없는 원자 객체에만 정의 된 값이 있다고 말합니다. 그러나 그것은 또한 휘발성 객체에 대한 접근이 관찰 가능한 부작용이라고 말합니다.
philipxy

1
@DavidSchwartz 일부 컴파일러 아키텍처 쌍은 실제 효과에 대한 표준 지정 액세스 시퀀스를 매핑하고 작업 프로그램은 이러한 효과를 얻기 위해 휘발성에 액세스합니다. 이러한 쌍에 매핑이 없거나 사소한 도움이되지 않는 매핑이 있다는 사실은 구현 품질과 관련이 있지만 현재 시점과는 관련이 없습니다.
philipxy 2014 년

25

C ++ volatile 키워드가 메모리 울타리를 도입합니까?

사양을 준수하는 C ++ 컴파일러는 메모리 펜스를 도입 할 필요가 없습니다. 특정 컴파일러는 할 수 있습니다. 컴파일러 작성자에게 질문을 보내십시오.

C ++에서 "휘발성"기능은 스레딩과 관련이 없습니다. "휘발성"의 목적은 외부 조건으로 인해 변경되는 레지스터에서 읽는 것이 최적화되지 않도록 컴파일러 최적화를 비활성화하는 것입니다. 다른 CPU의 다른 스레드에 의해 기록되는 메모리 주소가 외부 조건으로 인해 변경되는 레지스터입니까? 다시 말하지만, 일부 컴파일러 작성자가 외부 조건으로 인해 레지스터가 변경되는 것처럼 서로 다른 CPU의 서로 다른 스레드에 의해 기록되는 메모리 주소를 처리하도록 선택 했다면 그것이 바로 그들의 업무입니다. 그렇게 할 필요는 없습니다. 예를 들어 메모리 울타리를 도입하더라도 모든 스레드가 일관된 휘발성 읽기 및 쓰기 순서.

사실, volatile은 C / C ++의 스레딩에 거의 쓸모가 없습니다. 가장 좋은 방법은 그것을 피하는 것입니다.

또한 메모리 펜스는 특정 프로세서 아키텍처의 구현 세부 사항입니다. volatile 멀티 스레딩을 위해 명시 적 으로 설계된 C #에서는 프로그램이 처음에 펜스가없는 아키텍처에서 실행될 수 있기 때문에 사양에서 하프 펜스가 도입 될 것이라고 말하지 않습니다. 오히려 사양은 일부 부작용이 정렬되는 방식에 대한 특정 (매우 약한) 제약 조건을 설정하기 위해 컴파일러, 런타임 및 CPU가 어떤 최적화를 피할 것인지에 대한 확실한 (매우 약함) 보장을합니다. 실제로 이러한 최적화는 하프 펜스를 사용하여 제거되지만 이는 향후 변경 될 수있는 구현 세부 사항입니다.

멀티 스레딩과 관련된 모든 언어의 휘발성 의미에 관심이 있다는 사실은 스레드간에 메모리를 공유하는 것에 대해 생각하고 있음을 나타냅니다. 그렇게하지 않는 것을 고려하십시오. 이는 프로그램을 이해하기 훨씬 어렵게 만들고 미묘하고 재현 불가능한 버그를 포함 할 가능성이 훨씬 더 높습니다.


19
"휘발성은 C / C ++에서 거의 쓸모가 없습니다." 전혀! 사용자 모드 데스크톱 중심의 세계관을 가지고 있지만 대부분의 C 및 C ++ 코드는 메모리 매핑 I / O에 휘발성이 매우 필요한 임베디드 시스템에서 실행됩니다.
Ben Voigt

12
그리고 휘발성 액세스가 보존되는 이유는 단순히 외부 조건이 메모리 위치를 변경할 수 있기 때문이 아닙니다. 바로 액세스 자체가 추가 작업을 트리거 할 수 있습니다. 예를 들어 읽기가 FIFO를 진행하거나 인터럽트 플래그를 지우는 것은 매우 일반적입니다.
Ben Voigt 2014 년

3
@BenVoigt : 스레딩 문제를 효과적으로 처리하는 데 쓸모가 없었습니다.
Eric Lippert 2014 년

4
@DavidSchwartz 표준은 메모리 매핑 IO가 작동하는 방식을 보장 할 수 없습니다. 그러나 메모리 매핑 IO가 volatileC 표준에 도입 된 이유 입니다. 그러나 표준은 "액세스"에서 실제로 발생하는 것과 같은 것을 지정할 수 없기 때문에 "휘발성 한정 유형을 가진 객체에 대한 액세스를 구성하는 것은 구현 정의"라고 말합니다. 오늘날 너무 많은 구현은 액세스에 대한 유용한 정의를 제공하지 않습니다. IMHO는 문자를 준수하더라도 표준의 정신을 위반합니다.
제임스 간제

8
그 편집은 확실히 개선되었지만 당신의 설명은 여전히 ​​"메모리가 외생 적으로 변경 될 수있다"에 너무 초점을 맞추고 있습니다. volatile의미론은 그보다 더 강력합니다. 컴파일러는 요청 된 모든 액세스 (1.9 / 8, 1.9 / 12)를 생성해야하며 단순히 외인성 변경이 최종적으로 감지 (1.10 / 27)되도록 보장하는 것이 아닙니다. 메모리 매핑 된 I / O의 세계에서 메모리 읽기는 속성 getter와 같은 임의의 관련 논리를 가질 수 있습니다. 명시된 규칙에 따라 속성 getter에 대한 호출을 최적화 volatile하지 않으며 표준에서도 허용하지 않습니다.
Ben Voigt 2014 년

13

David가 간과하는 것은 C ++ 표준이 특정 상황에서만 상호 작용하는 여러 스레드의 동작을 지정하고 다른 모든 것은 정의되지 않은 동작을 초래한다는 사실입니다. 원자 변수를 사용하지 않으면 하나 이상의 쓰기와 관련된 경쟁 조건이 정의되지 않습니다.

따라서 CPU는 동기화 누락으로 인해 정의되지 않은 동작을 나타내는 프로그램의 차이 만 인식하므로 컴파일러는 동기화 명령을 무시할 권리가 있습니다.


5
잘 설명했습니다. 감사합니다. 표준 은 프로그램에 정의되지 않은 동작이없는 한 관찰 가능한 휘발성 물질에 대한 액세스 시퀀스 만 정의합니다 .
Jonathan Wakely

4
프로그램에 데이터 경쟁이있는 경우 표준은 프로그램의 관찰 가능한 동작에 대한 요구 사항을 만들지 않습니다. 컴파일러는 명시 적 장벽이나 원자 연산을 사용하여 프로그램에 존재하는 데이터 경합을 방지하기 위해 휘발성 액세스에 장벽을 추가하지 않을 것으로 예상됩니다. 이것이 프로그래머의 작업입니다.
Jonathan Wakely

왜 내가 그것을 간과하고 있다고 생각합니까? 내 주장의 어떤 부분이 무효화되었다고 생각하십니까? 나는 컴파일러가 동기화를 포기할 권리가 있다는 데 100 % 동의합니다.
David Schwartz

2
이것은 단순히 잘못되었거나 적어도 본질적인 것을 무시합니다. volatile스레드와 관련이 없습니다. 원래 목적은 메모리 매핑 IO를 지원하는 것이 었습니다. 그리고 적어도 일부 프로세서에서는 메모리 매핑 IO를 지원하려면 펜스가 필요합니다. (컴파일러는이 작업을 수행하지 않는다, 그러나 그것은 다른 문제입니다.)
제임스 간제에게

@JamesKanze volatile는 스레드와 많은 관련 volatile이 있습니다. 컴파일러가 액세스 할 수 있다는 사실을 알지 못해도 액세스 할 수있는 메모리를 다루고 특정 CPU의 스레드간에 공유 된 데이터의 실제 사용을 다룹니다.
curiousguy

12

우선, C ++ 표준은 원 자성이 아닌 읽기 / 쓰기를 올바르게 정렬하는 데 필요한 메모리 장벽을 보장하지 않습니다. 휘발성 변수는 MMIO, 신호 처리 등과 함께 사용하는 데 권장됩니다. 대부분의 구현에서 휘발성 은 멀티 스레딩에 유용하지 않으며 일반적으로 권장되지 않습니다.

휘발성 액세스의 구현과 관련하여 이것이 컴파일러 선택입니다.

gcc 동작을 설명하는 이 기사에서는 휘발성 메모리에 대한 쓰기 순서를 지정하기 위해 휘발성 객체를 메모리 장벽으로 사용할 수 없음을 보여줍니다.

icc 동작 과 관련 하여이 소스 는 휘발성이 메모리 액세스 순서를 보장하지 않는다고 말하고 있습니다.

Microsoft VS2013 컴파일러에는 다른 동작이 있습니다. 이 문서 는 volatile 이 Release / Acquire 의미론을 적용하고 휘발성 객체가 다중 스레드 애플리케이션의 잠금 / 해제에 사용되도록하는 방법을 설명합니다.

고려해야 할 또 다른 측면은 동일한 컴파일러가 wrt 동작다를 수 있다는 것 입니다. 대상 하드웨어 아키텍처에 따라 휘발성으로 . MSVS 2013 컴파일러에 관한 이 게시물 은 ARM 플랫폼 용 volatile로 컴파일하는 세부 사항을 명확하게 설명합니다.

그래서 내 대답 :

C ++ volatile 키워드가 메모리 울타리를 도입합니까?

될 것이다 : 아마,하지 보장하지만, 어떤 컴파일러는 그것을 할 수 없습니다. 그 사실에 의존해서는 안됩니다.


2
최적화를 방지하는 것이 아니라 컴파일러가 특정 제약 조건을 넘어서로드 및 저장을 변경하는 것을 방지 할뿐입니다.
Dietrich Epp 2014

당신이 무슨 말을하는지 명확하지 않습니다. 컴파일러가 volatile로드 / 스토어의 순서를 변경하지 못하게하는 일부 지정되지 않은 컴파일러의 경우에 해당한다고 말씀하시는 건가요 ? 아니면 C ++ 표준이 그렇게하도록 요구하고 있습니까? 후자의 경우 원래 질문에서 인용 된 반대에 대한 내 주장에 응답 할 수 있습니까?
David Schwartz

@DavidSchwartz 표준은 volatilelvalue를 통한 액세스의 순서 변경 (모든 소스에서)을 방지합니다 . 그러나 "액세스"의 정의를 구현에 남겨두기 때문에 구현이 신경 쓰지 않으면 우리를 많이 사지 않습니다.
제임스 간제

나는 MSC 컴파일러의 일부 버전에 대한 울타리의 의미를 구현했다고 생각 volatile되지만 울타리는 비주얼 스튜디오 2012에서 컴파일러에서 생성 된 코드에 없다
제임스 간제에게

@JamesKanze 기본적으로의 유일한 이식 가능 동작은 volatile표준에 의해 구체적으로 열거 된 것임을 의미합니다 . ( setjmp, 신호 등)
David Schwartz

7

컴파일러는 내가 아는 한 Itanium 아키텍처에만 메모리 펜스를 삽입합니다.

volatile키워드는 정말 최고의 비동기 예를 들어, 변경, 신호 처리기 및 메모리 매핑 레지스터에 사용됩니다; 일반적으로 다중 스레드 프로그래밍에 사용하는 것은 잘못된 도구입니다.


1
일종의. '컴파일러'(msvc)는 ARM 이외의 아키텍처를 대상으로하고 / volatile : ms 스위치가 사용되는 경우 (기본값) 메모리 펜스를 삽입합니다. msdn.microsoft.com/en-us/library/12a04hfd.aspx를 참조 하십시오 . 다른 컴파일러는 내가 아는 한 휘발성 변수에 울타리를 삽입하지 않습니다. 하드웨어, 시그널 핸들러 또는 비 c ++ 11 준수 컴파일러를 직접 다루지 않는 한 volatile의 사용은 피해야합니다.
Stefan

@Stefan No. volatile는 하드웨어를 다루지 않는 많은 용도에 매우 유용합니다. 구현시 C / C ++ 코드를 밀접하게 따르는 CPU 코드를 생성하려면 volatile.
curiousguy

7

컴파일러 "컴파일러"에 따라 다릅니다. Visual C ++는 2005 년부터 지원합니다. 그러나 표준에서는 필요하지 않으므로 다른 컴파일러에서는 필요하지 않습니다.


VC ++ 2012는 펜스를 삽입하지 않는 것 같습니다 : int volatile i; int main() { return i; }정확히 두 개의 명령어로 메인을 생성합니다 : mov eax, i; ret 0;.
제임스 간제

@JamesKanze : 정확히 어떤 버전입니까? 그리고 기본이 아닌 컴파일 옵션을 사용하고 있습니까? 나는 문서 (첫 번째 영향을받는 버전)(최신 버전) 에 의존하고 있는데, 확실히 획득 및 릴리스 의미론을 언급합니다.
Ben Voigt 2014 년

cl /help버전 18.00.21005.1을 말합니다. 그것이있는 디렉토리는 C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC입니다. 명령 창의 헤더에 VS 2013이 표시됩니다. 따라서 버전과 관련하여 ... 제가 사용한 유일한 옵션은 /c /O2 /Fa. (없이는 /O2로컬 스택 프레임도 설정합니다. 그러나 여전히 차단 명령은 없습니다.)
James Kanze

@JamesKanze : 아키텍처에 더 관심이있었습니다. "Microsoft (R) C / C ++ Optimizing Compiler Version 18.00.30723 for x64") 아마도 x86과 x64는 메모리 모델에서 상당히 강력한 캐시 일관성 보장을 가지고 있기 때문에 울타리가 없을 것입니다. ?
Ben Voigt 2014 년

아마도. 나는 정말로 모른다. 내가에서 이것을 main했기 때문에 컴파일러가 전체 프로그램을 볼 수 있고 다른 스레드가 없거나 적어도 내 전에 변수에 대한 다른 액세스가 없다는 것을 알 수 있다는 사실 (캐시 문제가 없을 수 있음)은 이것에 영향을 미칠 수 있습니다. 하지만 왠지 의심 스럽습니다.
James Kanze 2014 년

5

이것은 주로 메모리에서 발생하며 스레드없이 C ++ 11 이전 버전을 기반으로합니다. 그러나 커밋에서 스레딩에 대한 토론에 참여한 결과, volatile스레드 간의 동기화에 사용할 수있는 위원회의 의도는 없었습니다 . 마이크로 소프트가 제안했지만 그 제안은 이행되지 않았습니다.

의 주요 사양은 volatile휘발성에 대한 액세스가 IO와 마찬가지로 "관찰 가능한 동작"을 나타낸다는 것입니다. 같은 방식으로 컴파일러는 특정 IO를 재정렬하거나 제거 할 수 없으며, 휘발성 개체에 대한 액세스를 재정렬하거나 제거 할 수 없습니다 (또는 더 정확하게는 휘발성 한정 형식을 사용하여 lvalue 표현식을 통해 액세스). volatile의 원래 의도는 사실 메모리 매핑 IO를 지원하는 것이 었습니다. 그러나 이것의 "문제"는 "휘발성 액세스"를 구성하는 것이 정의 된 구현이라는 것입니다. 그리고 많은 컴파일러는 정의가 "메모리를 읽거나 쓰는 명령이 실행 된"것처럼 구현합니다. 구현이 지정 하면 쓸모없는 정의이지만 합법적 입니다. (아직 컴파일러에 대한 실제 사양을 찾지 못했습니다.

논쟁의 여지가 있지만 (그리고 내가 받아들이는 주장입니다) 이것은 하드웨어가 주소를 메모리 매핑 IO로 인식하고 재정렬 등을 금지하지 않는 한 표준의 의도를 위반합니다. 메모리 매핑 IO에 휘발성을 사용할 수도 없습니다. 적어도 Sparc 또는 Intel 아키텍처에서는. 그럼에도 불구하고 내가 본 어떤 컴파일러 (Sun CC, g ++ 및 MSC)도 펜스 또는 membar 명령을 출력하지 않습니다. (마이크로 소프트가 volatile. 그러나 내가 확인한 버전 (VS6.0 인 것 같음)은 펜스를 내 보내지 않았습니다.)


컴파일러 가 휘발성 객체에 대한 액세스를 재정렬하거나 제거 할 수 없다고 말하는 이유는 무엇 입니까? 확실히 액세스가 관찰 가능한 동작이라면 CPU, 쓰기 버퍼, 메모리 컨트롤러 및 기타 모든 항목이 재정렬하는 것을 방지하는 것도 똑같이 중요합니다.
David Schwartz

@DavidSchwartz 그것이 표준이 말하는 것이기 때문입니다. 확실히 실용적인 관점에서 볼 때 내가 확인한 컴파일러가하는 일은 완전히 쓸모가 없지만 표준 족제비 단어는 여전히 적합성을 주장 할 수있을 정도로 충분합니다 (또는 실제로 문서화 한 경우 가능).
제임스 간제

1
@DavidSchwartz : 주변 장치에 대한 배타적 (또는 뮤텍스) 메모리 매핑 I / O의 경우 volatile의미 체계가 완벽하게 적합합니다. 일반적으로 이러한 주변 장치는 메모리 영역을 캐시 불가능으로보고하므로 하드웨어 수준에서 재정렬하는 데 도움이됩니다.
Ben Voigt 2014 년

@BenVoigt 나는 그것에 대해 어떻게 든 궁금했다. 프로세서가 처리하는 주소가 메모리 매핑 IO라는 것을 어떻게 든 "알고"있다는 생각. 내가 아는 한, Sparcs는 이것을 지원하지 않으므로 Sparc의 Sun CC 및 g ++는 메모리 매핑 IO에 사용할 수 없습니다. (나는 이것으로 보았을 때, 나는 스팍 주로 관심이 있었다.)
제임스 간제

@JamesKanze : 내가 한 작은 검색에서 Sparc에는 캐시 할 수없는 메모리의 "대체보기"에 대한 전용 주소 범위가있는 것 같습니다. 휘발성 액세스 포인트 ASI_REAL_IO가 주소 공간 의 일부로 들어가는 한 괜찮습니다. (알테라의 NIOS는 MMU 바이 패스를 제어 주소의 높은 비트, 유사한 기술을 사용하여, 나는 확실히 다른 사람이있다하고있어 너무)
벤 보이트

5

그럴 필요가 없습니다. Volatile은 동기화 기본 요소가 아닙니다. 이는 최적화를 비활성화 할뿐입니다. 즉, 추상 기계에서 규정 한 것과 동일한 순서로 스레드 내에서 예측 가능한 읽기 및 쓰기 시퀀스를 얻습니다. 그러나 다른 스레드의 읽기 및 쓰기는 애초에 순서가 없습니다. 순서를 보존하거나 보존하지 않는다는 것은 의미가 없습니다. 광고 사이의 순서는 동기화 프리미티브에 의해 설정 될 수 있으며, 광고없이 UB를 얻을 수 있습니다.

메모리 장벽에 대한 약간의 설명. 일반적인 CPU에는 여러 수준의 메모리 액세스가 있습니다. 메모리 파이프 라인, 여러 수준의 캐시, RAM 등이 있습니다.

Membar 명령은 파이프 라인을 플러시합니다. 읽기와 쓰기가 실행되는 순서를 변경하지 않고 주어진 순간에 미해결 항목이 실행되도록 강제합니다. 멀티 스레드 프로그램에 유용하지만 그다지 많지는 않습니다.

캐시는 일반적으로 CPU간에 자동으로 일관됩니다. 캐시가 RAM과 동기화되어 있는지 확인하려면 캐시 플러시가 필요합니다. membar와는 매우 다릅니다.


1
그렇다면 C ++ 표준이 volatile컴파일러 최적화를 비활성화 한다고 말하는 것 입니까? 말이 안 돼. 컴파일러가 수행 할 수있는 모든 최적화는 적어도 원칙적으로 CPU가 똑같이 잘 수행 할 수 있습니다. 따라서 표준이 컴파일러 최적화를 비활성화했다고 말하면 이식 가능한 코드에서 의지 할 수있는 동작을 전혀 제공하지 않을 것입니다. 그러나 이식 가능한 코드는 setjmp및 신호 와 관련하여 동작에 의존 할 수 있기 때문에 분명히 사실이 아닙니다 .
David Schwartz

1
@DavidSchwartz 아니요, 표준에는 그런 것이 없습니다. 최적화를 비활성화하는 것은 표준을 구현하기 위해 일반적으로 수행되는 작업입니다. 표준은 관찰 가능한 동작이 추상 기계에서 요구하는 것과 동일한 순서로 발생하도록 요구합니다. 추상 기계가 주문을 필요로하지 않는 경우, 구현은 주문을 자유롭게 사용하거나 주문을 전혀 사용하지 않습니다. 다른 스레드의 휘발성 변수에 대한 액세스는 추가 동기화가 적용되지 않는 한 정렬되지 않습니다.
n. '대명사'm.

1
@DavidSchwartz 부정확 한 표현에 대해 사과드립니다. 이 표준은 최적화를 비활성화 할 것을 요구하지 않습니다. 최적화의 개념이 전혀 없습니다. 오히려 실제로 관찰 가능한 읽기 및 쓰기 시퀀스가 ​​표준을 준수하는 방식으로 컴파일러가 특정 최적화를 비활성화하도록 요구하는 동작을 지정합니다.
n. '대명사'm.

1
표준은 구현이 "읽기 및 쓰기의 관찰 가능한 시퀀스"를 원하는대로 정의 할 수 있도록 허용하기 때문에 필요하지 않습니다. 구현에서 최적화를 비활성화해야하는 관찰 가능한 시퀀스를 정의하도록 선택하면 그렇게됩니다. 그렇지 않다면 그렇지 않습니다. 구현에서 제공하기로 선택한 경우에만 예측 가능한 읽기 및 쓰기 시퀀스를 얻을 수 있습니다.
David Schwartz

1
아니요, 구현시 단일 액세스를 구성하는 항목을 정의해야합니다. 이러한 액세스의 순서는 추상 기계에 의해 규정됩니다. 구현은 순서를 유지해야합니다. 표준은 비록 비 규범적인 부분이기는하지만 "휘발성은 객체와 관련된 공격적인 최적화를 피하기위한 구현에 대한 힌트"라고 명시 적으로 말하고 있지만 의도는 분명합니다.
n. '대명사'm.

4

컴파일러 는 표준 작업 volatilevolatile지정된 용도를 만드는 데 필요한 경우에만 액세스 주위에 메모리 울타리를 도입해야합니다 (setjmp 특정 플랫폼 , 신호 처리기 등)에 .

일부 컴파일러는 volatile해당 플랫폼에서 더 강력하거나 유용하게 만들기 위해 C ++ 표준에서 요구하는 것 이상으로 이동합니다. 이식 가능한 코드는 volatileC ++ 표준에 지정된 것 이상의 작업을 수행 하는 데 의존해서는 안됩니다 .


2

나는 항상 인터럽트 서비스 루틴에서 휘발성을 사용합니다. 예를 들어 ISR (종종 어셈블리 코드)은 일부 메모리 위치를 수정하고 인터럽트 컨텍스트 외부에서 실행되는 상위 레벨 코드는 휘발성에 대한 포인터를 통해 메모리 위치에 액세스합니다.

RAM 및 메모리 매핑 된 IO에 대해이 작업을 수행합니다.

여기서 논의한 바에 따르면 이것은 여전히 ​​휘발성의 유효한 사용이지만 다중 스레드 또는 CPU와 관련이 없습니다. 마이크로 컨트롤러 용 컴파일러가 다른 액세스가있을 수 없다는 것을 "알면"(예 : 모든 것이 온칩이고 캐시가없고 코어가 하나뿐 임) 메모리 울타리가 전혀 암시되지 않는다고 생각합니다. 특정 최적화를 방지하기 만하면됩니다.

객체 코드를 실행하는 "시스템"에 더 많은 것을 쌓아두면 거의 모든 베팅이 해제됩니다. 적어도 이것이 제가이 토론을 읽는 방법입니다. 컴파일러가 어떻게 모든 기반을 다룰 수 있습니까?


0

휘발성 및 명령 재정렬에 대한 혼란은 CPU가 수행하는 재정렬의 두 가지 개념에서 기인한다고 생각합니다.

  1. 비 순차적 실행.
  2. 다른 CPU에서 볼 수있는 메모리 읽기 / 쓰기 시퀀스 (각 CPU가 다른 시퀀스를 볼 수 있다는 의미에서 재정렬).

휘발성은 단일 스레드 실행 (인터럽트 포함)을 가정하여 컴파일러가 코드를 생성하는 방법에 영향을줍니다. 이것은 메모리 배리어 명령어에 대한 어떤 것도 암시하지 않지만 오히려 컴파일러가 메모리 액세스와 관련된 특정 종류의 최적화를 수행하는 것을 배제합니다.
일반적인 예는 레지스터에 캐시 된 값을 사용하는 대신 메모리에서 값을 다시 가져 오는 것입니다.

비 순차적 실행

CPU는 최종 결과가 원래 코드에서 발생할 수있는 경우 비 순차적으로 / 추론 적으로 명령을 실행할 수 있습니다. 컴파일러는 모든 상황에서 올바른 변환 만 수행 할 수 있으므로 CPU는 컴파일러에서 허용되지 않는 변환을 수행 할 수 있습니다. 반대로, CPU는 이러한 최적화의 유효성을 확인하고 잘못된 것으로 판명되면 취소 할 수 있습니다.

다른 CPU에서 볼 수있는 메모리 읽기 / 쓰기 순서

명령어 시퀀스의 최종 결과 인 유효 순서는 컴파일러가 생성 한 코드의 의미와 일치해야합니다. 그러나 CPU가 선택한 실제 실행 순서는 다를 수 있습니다. 다른 CPU에서 볼 수있는 유효 순서 (모든 CPU는 다른보기를 가질 수 있음)는 메모리 장벽에 의해 제한 될 수 있습니다.
메모리 장벽이 CPU가 비 순차적 실행을 수행하는 것을 방해 할 수있는 정도를 모르기 때문에 얼마나 효과적이고 실제적인 순서가 다를 수 있는지 잘 모르겠습니다.

출처 :


0

현대 OpenGL로 작업하는 3D 그래픽 및 게임 엔진 개발을위한 온라인 다운로드 가능한 비디오 자습서를 진행하는 동안. 우리는 volatile수업 중 하나에서 사용했습니다 . 튜토리얼 웹 사이트는 여기 에서 찾을 수 있으며 volatile키워드로 작업하는 비디오 는 Shader Engine시리즈 비디오 98 에서 찾을 수 있습니다 .이 작업은 저의 작품이 아니지만 인증을 받았으며 Marek A. Krzeminski, MASc비디오 다운로드 페이지에서 발췌 한 것입니다.

"이제 게임을 여러 스레드에서 실행할 수 있으므로 스레드간에 데이터를 올바르게 동기화하는 것이 중요합니다.이 비디오에서는 volitile 변수가 제대로 동기화되도록하는 volitile locking 클래스를 만드는 방법을 보여줍니다 ..."

그리고 만약 당신이 그의 웹 사이트를 구독하고이 비디오에있는 그의 비디오에 접근 할 수 있다면 그는 with 사용에 관한 이 기사 를 참조합니다.Volatilemultithreading programming .

위 링크의 기사는 다음과 같습니다. http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

휘발성 : 멀티 스레드 프로그래머의 베스트 프렌드

Andrei Alexandrescu, 2001 년 2 월 1 일

volatile 키워드는 특정 비동기 이벤트가있을 때 코드를 잘못 렌더링 할 수있는 컴파일러 최적화를 방지하기 위해 고안되었습니다.

기분을 망치고 싶지는 않지만이 칼럼은 다중 스레드 프로그래밍이라는 두려운 주제를 다룹니다. Generic의 이전 기사에서 언급했듯이 예외 안전 프로그래밍이 어렵다면 다중 스레드 프로그래밍에 비해 어린이 놀이입니다.

여러 스레드를 사용하는 프로그램은 일반적으로 작성, 올바른 증명, 디버그, 유지 관리 및 길들이기가 어렵습니다. 잘못된 멀티 스레드 프로그램은 몇 년 동안 결함없이 실행될 수 있지만 일부 중요한 타이밍 조건이 충족 되었기 때문에 예기치 않게 실행될뿐입니다.

말할 필요도없이 멀티 스레드 코드를 작성하는 프로그래머는 얻을 수있는 모든 도움이 필요합니다. 이 칼럼은 경쟁 조건 (멀티 스레드 프로그램의 일반적인 문제 원인)에 초점을 맞추고이를 방지하는 방법에 대한 통찰력과 도구를 제공하고 놀랍게도 컴파일러가이를 해결하기 위해 열심히 노력하도록합니다.

약간의 키워드

비록 C와 C ++ 표준 모두 쓰레드에 관해서는 눈에 띄게 침묵하지만, volatile 키워드의 형태로 멀티 스레딩에 약간의 양보를합니다.

잘 알려진 const와 마찬가지로 volatile은 유형 수정 자입니다. 다른 스레드에서 액세스 및 수정되는 변수와 함께 사용하기위한 것입니다. 기본적으로 휘발성이 없으면 다중 스레드 프로그램 작성이 불가능하거나 컴파일러가 막대한 최적화 기회를 낭비합니다. 설명이 순서대로 있습니다.

다음 코드를 고려하십시오.

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

위의 Gadget :: Wait의 목적은 매초마다 flag_ 멤버 변수를 확인하고 다른 스레드에서 해당 변수가 true로 설정되었을 때 반환하는 것입니다. 적어도 그것은 프로그래머가 의도 한 것이지만, 아아, Wait는 틀 렸습니다.

컴파일러가 Sleep (1000)이 멤버 변수 flag_를 수정할 수없는 외부 라이브러리에 대한 호출이라고 판단한다고 가정합니다. 그런 다음 컴파일러는 레지스터에 flag_를 캐시하고 더 느린 온보드 메모리에 액세스하는 대신 해당 레지스터를 사용할 수 있다고 결론을 내립니다. 이것은 단일 스레드 코드에 대한 훌륭한 최적화이지만,이 경우 정확성에 해를 끼칩니다. Wait for some Gadget object를 호출 한 후에 다른 스레드가 Wakeup을 호출하더라도 Wait는 영원히 반복됩니다. 이는 flag_의 변경이 flag_를 캐시하는 레지스터에 반영되지 않기 때문입니다. 최적화도 낙관적입니다.

레지스터에 변수를 캐싱하는 것은 대부분의 시간에 적용되는 매우 가치있는 최적화이므로 낭비하는 것이 아쉽습니다. C 및 C ++는 이러한 캐싱을 명시 적으로 비활성화 할 수있는 기회를 제공합니다. 변수에 volatile 한정자를 사용하면 컴파일러는 해당 변수를 레지스터에 캐시하지 않습니다. 각 액세스는 해당 변수의 실제 메모리 위치에 도달합니다. 따라서 Gadget의 Wait / Wakeup 콤보 작업을 수행하기 위해해야 ​​할 일은 flag_를 적절하게 한정하는 것입니다.

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

휘발성 정지의 이론적 근거와 사용법에 대한 대부분의 설명은 여기에서 여러 스레드에서 사용하는 기본 유형을 휘발성으로 한정하도록 권장합니다. 그러나 C ++의 멋진 유형 시스템의 일부이기 때문에 volatile로 할 수있는 작업이 훨씬 더 많습니다.

사용자 정의 유형에 휘발성 사용

기본 유형뿐만 아니라 사용자 정의 유형도 volatile-qualify 할 수 있습니다. 이 경우 volatile은 const와 유사한 방식으로 유형을 수정합니다. (동일한 유형에 const와 volatile을 동시에 적용 할 수도 있습니다.)

const와 달리 volatile은 기본 유형과 사용자 정의 유형을 구분합니다. 즉, 클래스와 달리 기본 유형은 volatile로 한정 될 때 모든 연산 (더하기, 곱하기, 할당 등)을 계속 지원합니다. 예를 들어, 비 휘발성 int를 volatile int에 할당 할 수 있지만 비 휘발성 개체를 volatile 개체에 할당 할 수는 없습니다.

예제에서 volatile이 사용자 정의 유형에서 작동하는 방식을 설명하겠습니다.

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

휘발성이 물체에 그다지 유용하지 않다고 생각한다면 놀라움에 대비하십시오.

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

정규화되지 않은 유형에서 휘발성 대응 유형으로의 변환은 사소합니다. 그러나 const와 마찬가지로 휘발성에서 비정규로 되돌아 갈 수 없습니다. 캐스트를 사용해야합니다.

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

volatile로 한정된 클래스는 인터페이스의 하위 집합, 클래스 구현자가 제어하는 ​​하위 집합에만 액세스를 제공합니다. 사용자는 const_cast를 사용해야 만 해당 유형의 인터페이스에 대한 전체 액세스 권한을 얻을 수 있습니다. 또한 constness와 마찬가지로 volatileness는 클래스에서 해당 멤버로 전파됩니다 (예 : volatileGadget.name_ 및 volatileGadget.state_는 휘발성 변수 임).

휘발성, 중요 섹션 및 경쟁 조건

다중 스레드 프로그램에서 가장 간단하고 가장 자주 사용되는 동기화 장치는 뮤텍스입니다. 뮤텍스는 Acquire 및 Release 프리미티브를 노출합니다. 일부 스레드에서 Acquire를 호출하면 Acquire를 호출하는 다른 스레드가 차단됩니다. 나중에 해당 스레드가 Release를 호출하면 Acquire 호출에서 차단 된 정확히 하나의 스레드가 해제됩니다. 즉, 주어진 뮤텍스에 대해 Acquire 호출과 Release 호출 사이에 하나의 스레드 만 프로세서 시간을 가져올 수 있습니다. Acquire 호출과 Release 호출 사이의 실행 코드를 중요 섹션이라고합니다. (Windows 용어는 뮤텍스 자체를 중요한 섹션이라고 부르는 반면 "뮤텍스"는 실제로 프로세스 간 뮤텍스라고 부르기 때문에 약간 혼란 스럽습니다. 스레드 뮤텍스 및 프로세스 뮤텍스라고 부르면 좋았을 것입니다.)

뮤텍스는 경쟁 조건으로부터 데이터를 보호하는 데 사용됩니다. 정의에 따라 데이터에 대한 더 많은 스레드의 영향이 스레드가 예약 된 방식에 따라 달라지는 경우 경쟁 조건이 발생합니다. 두 개 이상의 스레드가 동일한 데이터를 사용하기 위해 경쟁 할 때 경쟁 조건이 나타납니다. 스레드는 임의의 순간에 서로를 인터럽트 할 수 있기 때문에 데이터가 손상되거나 잘못 해석 될 수 있습니다. 결과적으로 데이터에 대한 변경 및 때때로 액세스는 중요한 섹션으로 신중하게 보호되어야합니다. 객체 지향 프로그래밍에서 이것은 일반적으로 뮤텍스를 멤버 변수로 클래스에 저장하고 해당 클래스의 상태에 액세스 할 때마다 사용함을 의미합니다.

경험 많은 멀티 스레드 프로그래머는 위의 두 단락을 읽었을지 모르지만 그들의 목적은 지적 운동을 제공하는 것입니다. 이제 우리는 불안정한 연결과 연결될 것이기 때문입니다. 우리는 C ++ 유형의 세계와 스레딩 시맨틱 세계 사이에 병렬을 그려이를 수행합니다.

  • 중요 섹션 외부에서 스레드는 언제든지 다른 스레드를 인터럽트 할 수 있습니다. 제어가 없으므로 여러 스레드에서 액세스 할 수있는 변수는 휘발성입니다. 이는 컴파일러가 여러 스레드에서 사용하는 값을 한 번에 무의식적으로 캐싱하는 것을 방지하는 휘발성의 원래 의도와 일치합니다.
  • 뮤텍스에 의해 정의 된 중요 섹션 내에서는 하나의 스레드 만 액세스 할 수 있습니다. 따라서 중요한 섹션 내에서 실행 코드는 단일 스레드 의미 체계를 갖습니다. 제어 변수는 더 이상 휘발성이 아닙니다. 휘발성 한정자를 제거 할 수 있습니다.

간단히 말해, 스레드간에 공유되는 데이터는 개념적으로 중요 섹션 외부에서는 휘발성이고 중요 섹션 내부에서는 비 휘발성입니다.

뮤텍스를 잠그면 중요 섹션에 들어갑니다. const_cast를 적용하여 유형에서 휘발성 한정자를 제거합니다. 이 두 작업을 합치면 C ++의 유형 시스템과 응용 프로그램의 스레딩 의미 체계 사이에 연결이 생성됩니다. 컴파일러가 경쟁 조건을 확인하도록 만들 수 있습니다.

LockingPtr

뮤텍스 획득과 const_cast를 수집하는 도구가 필요합니다. 휘발성 객체 obj 및 뮤텍스 mtx로 초기화하는 LockingPtr 클래스 템플릿을 개발해 보겠습니다. 수명 동안 LockingPtr은 mtx를 획득 한 상태로 유지합니다. 또한 LockingPtr은 휘발성 제거 obj에 대한 액세스를 제공합니다. 운영자-> 및 운영자 *를 통해 스마트 포인터 방식으로 액세스가 제공됩니다. const_cast는 LockingPtr 내에서 수행됩니다. LockingPtr이 획득 한 뮤텍스를 수명 동안 유지하므로 캐스트는 의미 상 유효합니다.

먼저 LockingPtr이 작동 할 Mutex 클래스의 골격을 정의 해 보겠습니다.

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

LockingPtr을 사용하려면 운영 체제의 기본 데이터 구조와 기본 함수를 사용하여 Mutex를 구현합니다.

LockingPtr은 제어 변수의 유형으로 템플릿 화됩니다. 예를 들어 위젯을 제어하려면 휘발성 위젯 유형의 변수로 초기화하는 LockingPtr을 사용합니다.

LockingPtr의 정의는 매우 간단합니다. LockingPtr은 정교하지 않은 스마트 포인터를 구현합니다. const_cast 및 중요 섹션 수집에만 중점을 둡니다.

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

단순함에도 불구하고 LockingPtr은 올바른 다중 스레드 코드를 작성하는 데 매우 유용한 도구입니다. 스레드간에 공유되는 객체를 휘발성으로 정의하고 const_cast를 함께 사용하지 않아야합니다. 항상 LockingPtr 자동 객체를 사용하십시오. 예를 들어 설명해 봅시다.

벡터 객체를 공유하는 두 개의 스레드가 있다고 가정합니다.

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

스레드 함수 내에서 단순히 LockingPtr을 사용하여 buffer_ 멤버 변수에 대한 액세스를 제어합니다.

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

코드는 작성하고 이해하기가 매우 쉽습니다. buffer_를 사용해야 할 때마다이를 가리키는 LockingPtr을 만들어야합니다. 그렇게하면 벡터의 전체 인터페이스에 액세스 할 수 있습니다.

좋은 점은 실수하면 컴파일러가이를 지적한다는 것입니다.

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

const_cast를 적용하거나 LockingPtr을 사용할 때까지 buffer_의 기능에 액세스 할 수 없습니다. 차이점은 LockingPtr이 const_cast를 휘발성 변수에 적용하는 정렬 된 방법을 제공한다는 것입니다.

LockingPtr은 놀랍도록 표현력이 뛰어납니다. 하나의 함수 만 호출해야하는 경우 이름이 지정되지 않은 임시 LockingPtr 개체를 만들어 직접 사용할 수 있습니다.

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

원시 유형으로 돌아 가기

우리는 얼마나 휘발성이 제어되지 않는 액세스로부터 객체를 보호하는지와 LockingPtr이 스레드로부터 안전한 코드를 작성하는 간단하고 효과적인 방법을 제공하는 방법을 보았습니다. 이제 volatile로 다르게 처리되는 기본 유형으로 돌아가 보겠습니다.

여러 스레드가 int 유형의 변수를 공유하는 예를 살펴 보겠습니다.

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

Increment 및 Decrement가 다른 스레드에서 호출되는 경우 위의 조각은 버그가 있습니다. 첫째, ctr_은 휘발성이어야합니다. 둘째, ++ ctr_과 같이 원자 적으로 보이는 작업조차 실제로는 3 단계 작업입니다. 메모리 자체에는 산술 기능이 없습니다. 변수를 증가시킬 때 프로세서는 다음을 수행합니다.

  • 레지스터에서 해당 변수를 읽습니다.
  • 레지스터의 값을 증가시킵니다.
  • 결과를 다시 메모리에 씁니다.

이 3 단계 작업을 RMW (Read-Modify-Write)라고합니다. RMW 작업의 수정 부분 동안 대부분의 프로세서는 다른 프로세서가 메모리에 액세스 할 수 있도록 메모리 버스를 해제합니다.

그 때 다른 프로세서가 동일한 변수에 대해 RMW 연산을 수행하면 경쟁 조건이 생깁니다. 두 번째 쓰기가 첫 번째 쓰기의 효과를 덮어 씁니다.

이를 방지하려면 다시 LockingPtr을 사용할 수 있습니다.

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

이제 코드는 정확하지만 SyncBuf의 코드와 비교할 때 품질이 떨어집니다. 왜? Counter를 사용하면 컴파일러가 실수로 ctr_에 직접 액세스하는 경우 (잠그지 않고) 경고하지 않기 때문입니다. 컴파일러는 ctr_이 휘발성이면 ++ ctr_을 컴파일하지만 생성 된 코드는 단순히 올바르지 않습니다. 컴파일러는 더 이상 당신의 동맹이 아니며 오직 당신의 관심 만이 경쟁 조건을 피하는 데 도움이 될 수 있습니다.

그러면 어떻게해야합니까? 상위 레벨 구조에서 사용하는 기본 데이터를 캡슐화하고 해당 구조에 휘발성을 사용하십시오. 역설적이게도, 처음에는 이것이 휘발성의 사용 의도 였음에도 불구하고 빌트인과 함께 휘발성을 직접 사용하는 것이 더 나쁩니다!

휘발성 멤버 함수

지금까지 휘발성 데이터 멤버를 집계하는 클래스가 있습니다. 이제 더 큰 객체의 일부가되고 스레드간에 공유되는 클래스를 설계하는 것을 생각해 봅시다. 휘발성 멤버 함수가 큰 도움이 될 수있는 곳입니다.

클래스를 디자인 할 때 스레드로부터 안전한 멤버 함수 만 휘발성 한정합니다. 외부의 코드가 언제든지 모든 코드에서 휘발성 함수를 호출한다고 가정해야합니다. 잊지 마세요 : 휘발성은 자유 멀티 스레드 코드이며 중요 섹션이 없습니다. 비 휘발성은 단일 스레드 시나리오 또는 중요 섹션 내부와 같습니다.

예를 들어, 스레드로부터 안전한 것과 빠르고 보호되지 않는 것의 두 가지 변형으로 작업을 구현하는 위젯 클래스를 정의합니다.

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

오버로딩의 사용에 주목하십시오. 이제 Widget의 사용자는 휘발성 객체와 스레드 안전성을 확보하거나 일반 객체에 대해 균일 한 구문을 사용하여 Operation을 호출하고 속도를 얻을 수 있습니다. 사용자는 공유 위젯 객체를 휘발성으로 정의 할 때주의해야합니다.

휘발성 멤버 함수를 구현할 때 첫 번째 작업은 일반적으로 LockingPtr로이를 잠그는 것입니다. 그런 다음 비 휘발성 형제를 사용하여 작업이 수행됩니다.

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

요약

다중 스레드 프로그램을 작성할 때 휘발성을 유리하게 사용할 수 있습니다. 다음 규칙을 준수해야합니다.

  • 모든 공유 객체를 휘발성으로 정의하십시오.
  • 기본 유형에 volatile을 직접 사용하지 마십시오.
  • 공유 클래스를 정의 할 때 휘발성 멤버 함수를 사용하여 스레드 안전성을 표현하십시오.

이렇게하면 간단한 제네릭 구성 요소 LockingPtr을 사용하면 스레드로부터 안전한 코드를 작성하고 경쟁 조건에 대해 훨씬 덜 걱정할 수 있습니다. 컴파일러가 사용자를 걱정하고 부지런히 잘못된 부분을 지적하기 때문입니다.

내가 참여한 몇 가지 프로젝트는 volatile 및 LockingPtr을 사용하여 큰 효과를 냈습니다. 코드는 깨끗하고 이해하기 쉽습니다. 몇 가지 교착 상태가 기억 나지만 디버그가 훨씬 쉽기 때문에 경쟁 조건보다 교착 상태를 선호합니다. 경쟁 조건과 관련된 문제는 거의 없었습니다. 그러나 당신은 결코 알지 못합니다.

감사의 말

통찰력있는 아이디어를 제공하는 James Kanze와 Sorin Jianu에게 감사드립니다.


Andrei Alexandrescu는 워싱턴 주 시애틀에 기반을 둔 RealNetworks Inc. (www.realnetworks.com)의 개발 관리자이며 유명한 책 Modern C ++ Design의 저자입니다. 그는 www.moderncppdesign.com에서 연락 할 수 있습니다. Andrei는 또한 The C ++ Seminar (www.gotw.ca/cpp_seminar)의 주요 강사 중 한 명입니다.

이 기사는 약간 구식 일 수 있지만, 컴파일러가 경쟁 조건을 확인하는 동안 이벤트를 비동기 적으로 유지하는 데 도움이되는 다중 스레드 프로그래밍을 사용할 때 volatile 한정자를 사용하는 데있어 좋은 통찰력을 제공합니다. 이것은 메모리 펜스 생성에 대한 OP의 원래 질문에 직접적으로 대답하지 않을 수 있지만, 멀티 스레드 응용 프로그램으로 작업 할 때 휘발성을 잘 사용하는 것에 대한 훌륭한 참조로 다른 사람들을위한 대답으로 게시하기로 선택했습니다.


0

키워드는 volatile본질적으로 객체 읽기 및 쓰기가 프로그램에 의해 작성된대로 정확하게 수행 되어야하며 어떤 방식 으로든 최적화되지 않아야 함을 의미 합니다. 이진 코드는 C 또는 C ++ 코드를 따라야합니다 : 읽어지는로드, 쓰기가있는 저장소.

또한 어떤 읽기도 예측 가능한 값을 가져 오지 않아야 함을 의미합니다. 컴파일러는 동일한 휘발성 객체에 대한 쓰기 직후에도 읽기에 대해 어떤 것도 가정해서는 안됩니다.

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatile"C는 높은 수준의 어셈블리 언어입니다"도구 상자에서 가장 중요한 도구 일 수 있습니다 .

객체 휘발성 선언이 비동기 변경을 처리하는 코드의 동작을 보장하는 데 충분한 지 여부는 플랫폼에 따라 다릅니다. 다른 CPU는 정상적인 메모리 읽기 및 쓰기에 대해 서로 다른 수준의 보장 된 동기화를 제공합니다. 해당 분야의 전문가가 아니라면 이러한 저수준 멀티 스레딩 코드를 작성해서는 안됩니다.

아토믹 프리미티브는 코드에 대해 쉽게 추론 할 수 있도록 멀티 스레딩을위한 더 높은 수준의 객체보기를 제공합니다. 거의 모든 프로그래머는 뮤텍스, 읽기-쓰기 잠금, 세마포어 또는 기타 블로킹 기본 요소와 같은 상호 배제를 제공하는 원자 적 기본 요소 또는 기본 요소를 사용해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.