c++ - 키워드 - 아두이노 volatile 변수




C++ volatile 키워드는 메모리 펜스를 도입합니까? (9)

C ++ volatile 키워드는 메모리 펜스를 도입합니까?

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

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

실제로 휘발성은 C / C ++에서 스레딩에 거의 쓸모가 없습니다. 가장 좋은 방법은 피하는 것입니다.

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

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

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

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

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

Jonathan Wakely의 글을 참고하세요 :

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

David Schwartz 는 다음 과 같이 댓글에 합니다 .

... C ++ 표준의 관점에서, 무언가를 수행하는 컴파일러와 하드웨어가 무언가를하게하는 명령어를 방출하는 컴파일러 사이에는 차이가 없습니다. CPU가 휘발성 물질에 대한 액세스 순서를 재정렬 할 수있는 경우 표준에서는 순서를 유지하지 않아도됩니다. ...

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

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

다음 중 두 가지 질문이 나옵니다. 둘 중 어느 것이 "옳은가" 실제 구현은 실제로 무엇을합니까?


키워드는 volatile 본질적으로 개체를 읽고 쓰는 것이 프로그램에 의해 쓰여진대로 정확하게 수행되고 어떤 식 으로든 최적화되지 않아야 함을 의미 합니다. 이진 코드는 C 또는 C ++ 코드를 따라야합니다.이 코드를 읽는로드, 쓰기가있는 스토어.

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

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

volatile "C는 고급 어셈블리 언어"도구 상자에서 가장 중요한 도구 일 수 있습니다 .

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

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


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

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

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

우리가 객체 코드를 실행하는 "시스템"에 더 많은 자료를 쌓아 올리면 거의 모든 베팅이 해제됩니다. 컴파일러는 어떻게 모든 기반을 다룰 수 있습니까?


어떤 컴파일러 "컴파일러"인지에 따라 다릅니다. Visual C ++는 2005 년부터 사용하고 있습니다. 그러나 표준에는 필요하지 않으므로 다른 컴파일러에서는 필요하지 않습니다.


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

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

하드웨어가 주소를 메모리 매핑 된 IO로 인식하지 않고 재정렬 등을 금지하지 않으면 메모리 매핑 된 IO에 휘발성을 사용할 수 없기 때문에 표준의 의도를 위반합니다. 적어도 Sparc 또는 Intel 아키텍처에서는. 내가 본 컴파일러 (Sun CC, g ++ 및 MSC) 중 어느 것도 펜스 또는 membar 명령어를 출력하지 않습니다. (Microsoft가 volatile 대한 규칙 확장을 제안한 시간에 대해 일부 컴파일러가 제안을 구현하고 휘발성 액세스에 대한 차단 명령을 내렸다고 생각합니다. 최근 컴파일러의 기능을 확인하지는 않았지만 놀랍지 않습니다. 내가 확인한 버전 (VS6.0이라고 생각하지만)은 펜스를 방출하지 않았다.)


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

volatile 키워드는 신호 처리기 및 메모리 매핑 레지스터와 같은 비동기 변경에 가장 적합합니다. 멀티 스레드 프로그래밍에 사용하는 것은 일반적으로 잘못된 도구입니다.


필요하지 않습니다. 휘발성은 동기화 기본 요소가 아닙니다. 그것은 단지 최적화를 불가능하게한다. 즉, 추상 머신에 의해 규정 된 것과 동일한 순서로 스레드 내에서 예측 가능한 읽기 및 쓰기 시퀀스를 얻는다. 그러나 다른 스레드에서 읽고 쓰는 것은 처음에는 순서가 없으므로 순서를 유지하거나 유지하지 않는다고 말하는 것은 의미가 없습니다. thead 사이의 순서는 동기화 프리미티브에 의해 설정 될 수 있습니다.

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

Membar 명령어는 파이프 라인을 플러시합니다. 읽기 및 쓰기 실행 순서를 변경하지 않고 지정된 순간에 미해결 항목을 강제로 실행합니다. 다중 스레드 프로그램에는 유용하지만 그다지 많지는 않습니다.

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


현대 OpenGL로 작업하는 3D 그래픽 및 게임 엔진 개발을위한 온라인 다운로드 비디오 자습서를 진행하고있었습니다. 우리는 수업 중 하나에서 volatile 사용했습니다. 튜토리얼 웹 사이트는 여기 에서 찾을 수 있으며 volatile 키워드로 작업하는 비디오는 Shader Engine 시리즈 비디오 98에 있습니다. 다운로드 페이지.

"우리는 이제 여러 스레드에서 게임을 실행할 수 있기 때문에 스레드간에 데이터를 올바르게 동기화하는 것이 중요합니다.이 비디오에서는 변동 변수를 올바르게 동기화하기 위해 변동 잠금 클래스를 작성하는 방법을 보여줍니다 ..."

이 비디오에서 자신의 웹 사이트에 가입하고 자신의 비디오의에 액세스 할 수있는 경우 그리고 그는이 참조 article 의 사용에 관련된 Volatilemultithreading 프로그래밍.

위 링크의 기사는 다음과 같습니다. article

휘발성 : 멀티 스레드 프로그래머의 가장 친한 친구

2001 년 2 월 1 일 Andrei Alexandrescu

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

기분을 망치고 싶지는 않지만이 칼럼은 멀티 스레드 프로그래밍의 두려운 주제를 다룹니다. 이전의 제네릭 기사에서 예외 안전 프로그래밍이 어려운 경우 멀티 스레드 프로그래밍과 비교하여 어린이 놀이입니다.

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

말할 것도없이, 멀티 스레드 코드를 작성하는 프로그래머는 그녀가 얻을 수있는 모든 도움이 필요합니다. 이 칼럼은 다중 스레드 프로그램의 일반적인 문제 원인 인 경쟁 조건에 중점을두고이를 피하는 방법에 대한 통찰력과 도구를 제공하며 놀랍게도 컴파일러가이를 도와 주도록 열심히 노력합니다.

작은 키워드

C와 C ++ 표준은 모두 스레드에 대해 눈에 띄지 않지만 침묵적인 키워드 형태로 멀티 스레딩에 약간의 양보를합니다.

더 잘 알려진 대응 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_를 캐시하고 느린 온보드 메모리에 액세스하는 대신 해당 레지스터를 사용할 수 있다고 결론을 내립니다. 이것은 단일 스레드 코드에 대한 탁월한 최적화이지만이 경우 정확성에 해를 끼칩니다. 가젯 객체를 기다립니다. 다른 스레드가 Wakeup을 호출하더라도 대기는 영원히 반복됩니다. 이는 flag_의 변경이 flag_를 캐시하는 레지스터에 반영되지 않기 때문입니다. 최적화도 너무 낙관적입니다.

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

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

휘발성의 이론적 근거와 사용법에 대한 대부분의 설명은 여기서 중지하고 여러 스레드에서 사용하는 기본 유형을 휘발성으로 한정하는 것이 좋습니다. 그러나 C ++의 훌륭한 유형 시스템의 일부이기 때문에 휘발성으로 훨씬 더 많은 일을 할 수 있습니다.

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

기본 유형뿐만 아니라 사용자 정의 유형도 휘발성으로 한정 할 수 있습니다. 이 경우 volatile은 const와 비슷한 방식으로 유형을 수정합니다. const와 volatile을 같은 유형에 동시에 적용 할 수도 있습니다.

const와 달리 휘발성은 기본 유형과 사용자 정의 유형을 구분합니다. 즉, 클래스와 달리 기본 유형은 변동이있을 때 여전히 모든 연산 (더하기, 곱하기, 대입 등)을 지원합니다. 예를 들어, 비 휘발성 int를 휘발성 int에 할당 할 수 있지만 비 휘발성 객체는 휘발성 객체에 할당 할 수 없습니다.

예제에서 사용자 정의 형식에 대해 휘발성이 어떻게 작동하는지 설명해 보겠습니다.

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

휘발성으로 인증 된 클래스는 인터페이스의 서브 세트, 클래스 구현자가 제어하는 ​​서브 세트에만 액세스 할 수 있습니다. 사용자는 const_cast를 사용해야 만 해당 유형의 인터페이스에 완전히 액세스 할 수 있습니다. 또한 constness와 마찬가지로 휘발성은 클래스에서 멤버로 전파됩니다 (예 : volatileGadget.name_ 및 volatileGadget.state_는 휘발성 변수입니다).

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

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

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

숙련 된 멀티 스레드 프로그래머는 위의 두 단락을 읽었을 수도 있지만, 그 목적은 지적 운동을 제공하는 것입니다. 우리는 C ++ 타입의 세계와 스레딩 의미론 세계 사이에 평행을 그려서이를 수행합니다.

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

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

뮤텍스를 잠그면 중요한 섹션에 들어갑니다. const_cast를 적용하여 유형에서 휘발성 한정자를 제거합니다. 이 두 연산을 합치면 C ++의 타입 시스템과 애플리케이션의 스레딩 시맨틱 사이에 연결을 만듭니다. 컴파일러가 경쟁 조건을 확인하도록 할 수 있습니다.

잠금 장치

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

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

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

LockingPtr을 사용하려면 운영 체제의 기본 데이터 구조 및 기본 기능을 사용하여 Mutex를 구현하십시오.

LockingPtr은 제어 변수의 유형으로 템플릿됩니다. 예를 들어, 위젯을 제어하려는 경우 volatile Widget 유형의 변수로 초기화하는 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이 스레드 안전 코드를 작성하는 간단하고 효과적인 방법을 제공하는지 보았습니다. 휘발성에 따라 다르게 처리되는 기본 유형으로 돌아 갑시다.

여러 스레드가 int 유형의 변수를 공유하는 예를 고려해 봅시다.

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

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

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

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

그때 다른 프로세서가 동일한 변수에 대해 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_;
};

과부하 사용에 주목하십시오. 이제 위젯 사용자는 휘발성 객체 및 스레드 안전성을 얻거나 일반 객체 및 속도를 얻기 위해 통일 된 구문을 사용하여 오퍼레이션을 호출 할 수 있습니다. 사용자는 공유 위젯 오브젝트를 일시적으로 정의 할 때주의해야합니다.

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

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

요약

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

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

이 작업을 수행하고 간단한 일반 구성 요소 인 LockingPtr을 사용하는 경우 컴파일러에서 걱정하고 부적절한 부분을 지적하기 때문에 스레드 안전 코드를 작성하고 경쟁 조건에 대해 훨씬 덜 걱정할 수 있습니다.

내가 참여한 몇 가지 프로젝트는 휘발성 및 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)의 주요 강사 중 한 명입니다.

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


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 다른 스레드 쓰기가있는 경우 위 예제는 작동하지 않습니다.

다른 모든 경우에, volatile 사용은 이식 불가능한 것으로 간주되어야하며, C ++ 11 이전의 컴파일러 및 컴파일러 확장 (예 : msvc의 /volatile:ms 스위치)을 처리 할 때를 제외하고는 더 이상 코드 검토를 통과하지 않아야합니다 /volatile:ms X86 / I64).





volatile