programing tip

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

itbloger 2020. 10. 8. 07:45
반응형

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


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

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


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

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

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

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

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

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

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

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


무엇을하는지 설명하는 대신 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의 스위치)을 처리하는 경우를 제외하고는을 (를) 이식 할 수없는 것으로 간주하고 더 이상 코드 검토를 통과하지 않아야 합니다.


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

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

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

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

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

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


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

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


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

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

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

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

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

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

그래서 내 대답 :

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

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


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

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


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


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

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

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


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

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

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

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


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

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


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

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

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

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


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

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

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

비 순차적 실행

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

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

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

출처 :


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

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

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

위 링크의 기사는 다음과 같습니다. 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 객체를 호출 한 후에 다른 스레드가 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와 마찬가지로 휘발성은 클래스에서 해당 멤버로 전파됩니다 (예 : 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은 올바른 다중 스레드 코드를 작성하는 데 매우 유용한 도구입니다. 스레드간에 공유되는 객체를 volatile로 정의하고 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 단계 작업입니다. 메모리 자체에는 산술 기능이 없습니다. 변수를 증가시킬 때 프로세서는 다음을 수행합니다.

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

This three-step operation is called RMW (Read-Modify-Write). During the Modify part of an RMW operation, most processors free the memory bus in order to give other processors access to the memory.

If at that time another processor performs a RMW operation on the same variable, we have a race condition: the second write overwrites the effect of the first.

To avoid that, you can rely, again, on LockingPtr:

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

Now the code is correct, but its quality is inferior when compared to SyncBuf's code. Why? Because with Counter, the compiler will not warn you if you mistakenly access ctr_ directly (without locking it). The compiler compiles ++ctr_ if ctr_ is volatile, although the generated code is simply incorrect. The compiler is not your ally anymore, and only your attention can help you avoid race conditions.

What should you do then? Simply encapsulate the primitive data that you use in higher-level structures and use volatile with those structures. Paradoxically, it's worse to use volatile directly with built-ins, in spite of the fact that initially this was the usage intent of volatile!

volatile Member Functions

So far, we've had classes that aggregate volatile data members; now let's think of designing classes that in turn will be part of larger objects and shared between threads. Here is where volatile member functions can be of great help.

When designing your class, you volatile-qualify only those member functions that are thread safe. You must assume that code from the outside will call the volatile functions from any code at any time. Don't forget: volatile equals free multithreaded code and no critical section; non-volatile equals single-threaded scenario or inside a critical section.

For example, you define a class Widget that implements an operation in two variants — a thread-safe one and a fast, unprotected one.

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

Notice the use of overloading. Now Widget's user can invoke Operation using a uniform syntax either for volatile objects and get thread safety, or for regular objects and get speed. The user must be careful about defining the shared Widget objects as volatile.

When implementing a volatile member function, the first operation is usually to lock this with a LockingPtr. Then the work is done by using the non- volatile sibling:

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

Summary

When writing multithreaded programs, you can use volatile to your advantage. You must stick to the following rules:

  • Define all shared objects as volatile.
  • Don't use volatile directly with primitive types.
  • When defining shared classes, use volatile member functions to express thread safety.

If you do this, and if you use the simple generic component LockingPtr, you can write thread-safe code and worry much less about race conditions, because the compiler will worry for you and will diligently point out the spots where you are wrong.

A couple of projects I've been involved with use volatile and LockingPtr to great effect. The code is clean and understandable. I recall a couple of deadlocks, but I prefer deadlocks to race conditions because they are so much easier to debug. There were virtually no problems related to race conditions. But then you never know.

Acknowledgements

Many thanks to James Kanze and Sorin Jianu who helped with insightful ideas.


Andrei Alexandrescu is a Development Manager at RealNetworks Inc. (www.realnetworks.com), based in Seattle, WA, and author of the acclaimed book Modern C++ Design. He may be contacted at www.moderncppdesign.com. Andrei is also one of the featured instructors of The C++ Seminar (www.gotw.ca/cpp_seminar).

This article might be a little dated, but it does give good insight towards an excellent use of using the volatile modifier with in the use of multithreaded programming to help keep events asynchronous while having the compiler checking for race conditions for us. This may not directly answer the OPs original question about creating a memory fence, but I choose to post this as an answer for others as an excellent reference towards a good use of volatile when working with multithreaded applications.


The keyword volatile essentially means that reads and writes an object should be performed exactly as written by the program, and not optimized in any way. Binary code should follow C or C++ code: a load where this is read, a store where there is a write.

It also means that no read should be expected to result in a predictable value: the compiler shouldn't assume anything about a read even immediately following a write to the same volatile object:

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

volatile may be the most important tool in the "C is a high level assembly language" toolbox.

Whether declaring an object volatile is sufficient for ensuring the behavior of code that deals with asynchronous changes depends on the platform: different CPU give different levels of guaranteed synchronization for normal memory reads and writes. You probably shouldn't try to write such low level multithreading code unless you are an expert in the area.

Atomic primitives provide a nice higher level view of objects for multithreading that makes it easy to reason about code. Almost all programmers should use either atomic primitives or primitives that provide mutual exclusions like mutexes, read-write-locks, semaphores, or other blocking primitives.

참고URL : https://stackoverflow.com/questions/26307071/does-the-c-volatile-keyword-introduce-a-memory-fence

반응형