휘발성이 멀티 스레드 C 또는 C ++ 프로그래밍에서 유용하지 않은 이유는 무엇입니까?


165

최근에 게시 한이 답변 에서 입증 된 것처럼 volatile멀티 스레드 프로그래밍 컨텍스트 의 유틸리티 (또는 부족)에 대해 혼란스러워하는 것 같습니다 .

내 이해는 이것입니다 : 변수가 액세스하는 코드 조각의 제어 흐름 외부에서 변수가 변경 될 때마다 변수는로 선언되어야합니다 volatile. 다른 스레드에 의해 수정 된 신호 처리기, I / O 레지스터 및 변수는 모두 이러한 상황을 구성합니다.

따라서 전역 int foo가 있고 foo하나의 스레드에서 읽고 다른 스레드에서 원자 적으로 설정 한 경우 (아마도 적절한 기계 명령어 사용) 읽기 스레드는 신호 처리기에 의해 조정 된 변수를 보는 것과 같은 방식 으로이 상황을 봅니다. 외부 하드웨어 조건에 의해 수정되므로 foo선언해야합니다 volatile(또는 멀티 스레드 상황의 경우 메모리 고정로드로 액세스하는 것이 더 나은 솔루션 일 수 있습니다).

어떻게 그리고 어디에서 틀렸습니까?


7
휘발성은 컴파일러가 휘발성 변수에 대한 액세스를 캐시해서는 안된다는 것입니다. 그러한 액세스를 직렬화하는 것에 대해서는 아무 것도 말하지 않습니다. 이것은 여기에서 논의되었습니다. 나는 몇 번인지 알지 못하며,이 질문이 그 토론에 아무것도 추가하지 않을 것이라고 생각합니다.

4
그리고 또 다시, 그것을받을 자격이없는 질문은 여기에 여러 번 질문되었습니다 upvoted. 제발 그만해

14
@neil 다른 질문을 찾아서 하나를 찾았지만 기존의 설명을 통해 내가 왜 틀린지 실제로 이해하는 데 필요한 것을 유발하지는 않았습니다. 이 질문은 그러한 대답을 이끌어 냈습니다.
Michael Ekstrand

1
CPU가 (자신의 캐시를 통해) 데이터로 무엇에 큰 깊이있는 연구를 위해 체크 아웃 : rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot

1
@curiousguy 이것이 하드웨어 레지스터 등에 쓰는 데 사용될 수 있고 Java에서 일반적으로 사용되는 것처럼 멀티 스레딩에 사용되지 않는 "C의 경우가 아닙니다"라는 의미입니다.
Monstieur

답변:


213

volatile다중 스레드 컨텍스트에서 문제 는 필요한 모든 보장을 제공하지 않는다는 것 입니다. 필요한 속성이 몇 개 있지만 전부는 아니기 때문에 volatile 혼자 의지 할 수는 없습니다 .

그러나 나머지 속성 에 사용해야하는 프리미티브 도 그 속성을 제공 volatile하므로 사실상 불필요합니다.

공유 데이터에 스레드로부터 안전하게 액세스하려면 다음을 보장해야합니다.

  • 실제로 읽기 / 쓰기가 발생합니다 (컴파일러는 레지스터에 값을 저장하지 않고 나중에 나중에 주 메모리 업데이트를 연기하지 않습니다)
  • 재주문이 발생하지 않습니다. volatile변수를 플래그로 사용하여 일부 데이터를 읽을 준비가되었는지 여부를 표시 한다고 가정하십시오 . 코드에서는 데이터를 준비한 후 플래그를 설정하기 만하면 모든 것이 잘 보입니다 . 그러나 명령이 재정렬되어 플래그가 먼저 설정되면 어떻게 될까요?

volatile첫 번째 포인트를 보장합니다. 또한 다른 휘발성 읽기 / 쓰기간에 재정렬이 발생하지 않도록합니다 . 모든 volatile메모리 액세스는 지정된 순서대로 수행됩니다. volatileI / O 레지스터 또는 메모리 매핑 된 하드웨어를 조작하기 위해 필요한 모든 것이지만, volatile객체가 비 휘발성 데이터에 대한 액세스를 동기화하는 데만 사용되는 멀티 스레드 코드에서는 도움이되지 않습니다 . 이러한 액세스는 여전히 액세스와 관련하여 재정렬 될 수 volatile있습니다.

재정렬을 방지하는 해결책은 메모리 배리어 를 사용하는 것입니다. 메모리 배리어이 시점에서 메모리 액세스가 재정렬되지 않을 수 있음 을 컴파일러와 CPU에 모두 나타냅니다 . 휘발성 가변 액세스 주위에 이러한 장벽을 배치하면 비 휘발성 액세스도 휘발성 액세스에서 재정렬되지 않으므로 스레드 안전 코드를 작성할 수 있습니다.

그러나 메모리 장벽 매우 효과적으로 우리에게 우리가 만드는 자체가 필요로하는 모든 것을 제공, 대기중인 모든이 쓰기가 장벽에 도달하면 실행 / 읽기 수 있도록 volatile필요합니다. volatile한정자를 완전히 제거 할 수 있습니다 .

C ++ 11부터 원자 변수 ( std::atomic<T>)는 모든 관련 보증을 제공합니다.


5
@jbcreix : 어떤 "그것"에 대해 물어보고 있습니까? 휘발성 또는 메모리 장벽? 어쨌든 대답은 거의 같습니다. 그들은 프로그램의 관찰 가능한 행동을 설명하기 때문에 컴파일러와 CPU 수준에서 모두 작동해야합니다. 따라서 CPU가 모든 것을 재정렬하여 보장되는 행동을 변경하지 않아야합니다. 그러나 메모리 장벽은 표준 C ++의 일부가 아니므로 이식성이 없기 때문에 현재 휴대용 스레드 동기화를 작성할 수 없습니다 volatile.
jalf

4
MSDN 예제에서이 작업을 수행하고 일시적 액세스를 통해 명령을 다시 정렬 할 수 없다고 주장합니다. msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW

27
@OJW : 그러나 Microsoft의 컴파일러 volatile는 전체 메모리 장벽으로 재정의 합니다 (재주문 방지). 이는 표준의 일부가 아니므로 이식 가능한 코드에서는이 동작에 의존 할 수 없습니다.
jalf

4
@Skizz : 아니오, 그것이 방정식의 "컴파일러 매직"부분이 나오는 곳입니다. 메모리 장벽은 CPU와 컴파일러 모두 에 의해 이해되어야 합니다 . 컴파일러가 메모리 장벽의 의미를 이해하면, 그러한 장벽을 피하는 것뿐만 아니라 장벽을 가로 질러 읽기 / 쓰기 순서를 다시 지정할 수 있습니다. 운 좋게도 컴파일러 메모리 장벽의 의미를 이해하므로 결국 모든 것이 잘 작동합니다. :)
jalf

13
@Skizz : 스레드 자체는 항상 C ++ 11 및 C11 이전의 플랫폼 종속 확장입니다. 내가 아는 한, 스레딩 확장을 제공하는 모든 C 및 C ++ 환경은 "메모리 장벽"확장도 제공합니다. 그럼에도 불구하고, volatile멀티 스레드 프로그래밍에는 항상 쓸모가 없습니다. (Visual Studio에서는 휘발성 메모리 장벽 확장입니다.)
Nemo

49

Linux Kernel Documentation 에서 이것을 고려할 수도 있습니다 .

C 프로그래머들은 종종 현재 실행 스레드 외부에서 변수가 변경 될 수 있음을 의미하기 위해 일시적으로 변합니다. 결과적으로 공유 데이터 구조를 사용할 때 커널 코드에서 사용하려고하는 경우가 있습니다. 다시 말해, 그들은 휘발성 유형을 일종의 쉬운 원자 변수로 취급하는 것으로 알려져 있습니다. 커널 코드에서 휘발성을 사용하는 것은 거의 정확하지 않습니다. 이 문서는 이유를 설명합니다.

휘발성과 관련하여 이해해야 할 핵심은 그 목적이 최적화를 억제하는 것인데, 이는 실제로는 절대로 원하지 않는 것입니다. 커널에서, 원치 않는 동시 액세스로부터 공유 데이터 구조를 보호해야합니다. 이는 매우 다른 작업입니다. 원치 않는 동시성을 방지하는 프로세스는 거의 모든 최적화 관련 문제를보다 효율적으로 피할 수 있습니다.

휘발성과 마찬가지로 데이터에 동시에 액세스 할 수있는 커널 프리미티브 (스핀 록, 뮤텍스, 메모리 장벽 등)는 원하지 않는 최적화를 방지하도록 설계되었습니다. 그것들이 올바르게 사용된다면, 휘발성도 사용할 필요가 없습니다. 휘발성이 여전히 필요한 경우 코드에 버그가있을 수 있습니다. 올바르게 작성된 커널 코드에서 휘발성은 작업 속도를 늦추는 역할 만 할 수 있습니다.

일반적인 커널 코드 블록을 고려하십시오.

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

모든 코드가 잠금 규칙을 따르는 경우 the_lock이 유지되는 동안 shared_data의 값이 예기치 않게 변경 될 수 없습니다. 해당 데이터로 재생하려는 다른 코드는 잠금 대기 중입니다. spinlock 프리미티브는 메모리 배리어 역할을합니다. 명시 적으로 작성되었으므로 데이터 액세스가 최적화되지 않습니다. 따라서 컴파일러는 shared_data에 무엇이 있는지 알고 있다고 생각할 수 있지만 spin_lock () 호출은 메모리 장벽으로 작동하므로 알고있는 것을 잊게합니다. 해당 데이터에 액세스 할 때 최적화 문제가 없습니다.

shared_data가 휘발성으로 선언 된 경우 여전히 잠금이 필요합니다. 그러나 컴파일러가 다른 사람과 함께 작업 할 수 없다는 것을 알고 있으면 중요한 섹션 에서 shared_data에 대한 액세스를 최적화하지 못하게됩니다 . 잠금이 유지되는 동안 shared_data는 휘발성이 아닙니다. 공유 데이터를 처리 할 때 적절한 잠금은 불필요하고 잠재적으로 유해합니다.

휘발성 스토리지 클래스는 원래 메모리 매핑 된 I / O 레지스터를위한 것입니다. 커널 내에서 레지스터 액세스도 잠금으로 보호해야하지만 컴파일러는 중요한 섹션 내에서 레지스터 액세스를 "최적화"하기를 원하지 않습니다. 그러나 커널 내에서 I / O 메모리 액세스는 항상 접근 자 기능을 통해 수행됩니다. 포인터를 통해 I / O 메모리에 직접 액세스하는 것은 어리둥절하며 모든 아키텍처에서 작동하지는 않습니다. 이러한 접근자는 원치 않는 최적화를 방지하기 위해 작성되므로 휘발성은 다시 한 번 불필요합니다.

휘발성을 사용하려는 유혹을받는 또 다른 상황은 프로세서가 변수 값을 기다리는 중입니다. 바쁜 대기를 수행하는 올바른 방법은 다음과 같습니다.

while (my_variable != what_i_want)
    cpu_relax();

cpu_relax () 호출은 CPU 전력 소비를 낮추거나 하이퍼 스레드 트윈 프로세서에 대한 수율을 제공합니다. 또한 메모리 장벽 역할을하기 때문에 다시 한번 휘발성이 필요하지 않습니다. 물론, 바쁘게 기다리는 것은 일반적으로 시작하는 반 사회적 행동입니다.

커널에서 휘발성이 의미가있는 몇 가지 드문 상황이 여전히 있습니다.

  • 위에서 언급 한 접근 자 함수는 직접 I / O 메모리 액세스가 작동하는 아키텍처에서 휘발성을 사용할 수 있습니다. 기본적으로 각 접근 자 호출은 자체적으로 약간 중요한 부분이되고 프로그래머가 예상 한대로 액세스가 이루어 지도록합니다.

  • 메모리를 변경하지만 다른 눈에 띄는 부작용이없는 인라인 어셈블리 코드는 GCC에 의해 삭제 될 위험이 있습니다. volatile 키워드를 asm 문에 추가하면 이러한 제거가 방지됩니다.

  • jiffies 변수는 참조 될 때마다 다른 값을 가질 수 있다는 점에서 특별하지만 특별한 잠금없이 읽을 수 있습니다. 따라서 지프는 휘발성이 될 수 있지만이 유형의 다른 변수를 추가하는 것은 매우 어려워집니다. Jiffies는 이와 관련하여 "멍청한 유산"문제 (Linus의 말)로 간주됩니다. 그것을 고치는 것은 가치보다 더 문제가 될 것입니다.

  • I / O 장치에 의해 수정 될 수있는 코 히어 런트 메모리의 데이터 구조에 대한 포인터는 때때로 합법적으로 휘발성 일 수 있습니다. 네트워크 어댑터가 사용하는 링 버퍼 (어댑터가 포인터를 변경하여 처리 된 디스크립터를 표시 함)는 이러한 유형의 상황의 예입니다.

대부분의 코드에서 휘발성에 대한 위의 근거 중 어느 것도 적용되지 않습니다. 결과적으로 휘발성의 사용은 버그로 보일 수 있으며 코드를 추가로 조사합니다. 휘발성을 사용하려는 개발자는 한 발짝 물러서서 진정으로 달성하려는 것에 대해 생각해야합니다.


3
@curiousguy : 그렇습니다. gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html 도 참조하십시오 .
Sebastian Mach

1
spin_lock ()은 일반 함수 호출처럼 보입니다. 컴파일러가 특별히 처리하여 생성 된 코드가 spin_lock () 이전에 읽혀지고 레지스터에 저장된 모든 shared_data 값을 "잊어 버려"값을 새로 읽어야한다는 점에서 특별한 점은 spin_lock () 다음에 do_something_on ()?
Syncopated

1
@underscore_d 내 요점은 함수 이름 spin_lock ()으로부터 특별한 일을 할 수 없다는 것입니다. 나는 그것이 무엇인지 모른다. 특히, 컴파일러가 후속 읽기를 최적화하지 못하게하는 구현의 내용을 모르겠습니다.
Syncopated

1
Syncopated는 좋은 지적이 있습니다. 이것은 본질적으로 프로그래머가 이러한 "특수 기능"의 내부 구현을 알고 있거나 적어도 그들의 행동에 대해 잘 알고 있어야한다는 것을 의미합니다. 이러한 특수 기능이 모든 아키텍처와 모든 컴파일러에서 동일한 방식으로 작동하도록 표준화되고 보장됩니까? 사용 가능한 기능 목록이 있습니까, 아니면 최소한 코드 주석을 사용하여 개발자에게 문제의 기능이 "최적화"되지 않도록 코드를 보호한다는 신호를 보내는 규칙이 있습니까?
JustAMartin

1
@Tuntable : 개인 정적은 포인터를 통해 어떤 코드로든 만질 수 있습니다. 그리고 그 주소가 취해지고 있습니다. 아마도 데이터 흐름 분석은 포인터가 결코 이스케이프하지 않는다는 것을 증명할 수 있지만 일반적으로 프로그램 크기가 초 선형이라는 매우 어려운 문제입니다. 별칭이 존재하지 않는 것을 보장 할 수있는 경우 스핀 잠금을 통해 액세스를 이동하면 실제로 정상입니다. 그러나 별명이 없으면 volatile무의미합니다. 모든 경우에, "본문을 볼 수없는 함수 호출"동작이 올 바릅니다.
벤 Voigt

11

나는 당신이 틀렸다고 생각하지 않습니다-값이 스레드 A 이외의 다른 값으로 변경되면 스레드 A가 값 변경을 볼 수 있도록 휘발성이 필요합니다. volatile은 기본적으로 컴파일러는 "이 변수를 레지스터에 캐시하지 말고 항상 액세스 할 때마다 항상 RAM 메모리에서 변수를 읽고 쓰십시오".

혼란은 많은 것들을 구현하기에 충분하지 않기 때문입니다. 특히 최신 시스템은 여러 수준의 캐싱을 사용하고 최신 멀티 코어 CPU는 런타임에 멋진 최적화를 수행하고 최신 컴파일러는 컴파일 타임에 멋진 최적화를 수행하므로 다양한 부작용이 다르게 나타날 수 있습니다. 소스 코드를 보았을 때 예상 한 순서대로 주문하십시오.

휘발성 변수의 '관측 된'변화가 예상대로 정확하게 발생하지 않을 수 있다는 것을 명심하는 한 휘발성은 좋습니다. 특히 휘발성 변수는 스레드간에 작업을 동기화하거나 순서를 지정하는 방법으로 사용하지 마십시오. 안정적으로 작동하지 않기 때문입니다.

개인적으로 휘발성 플래그에 대한 나의 주요한 사용 (전용?)은 "pleaseGoAwayNow"부울입니다. 계속 반복되는 작업자 스레드가있는 경우 루프의 각 반복에서 휘발성 부울을 확인하고 부울이 참이면 종료합니다. 그런 다음 기본 스레드는 부울을 true로 설정 한 다음 pthread_join ()을 호출하여 작업자 스레드가 없어 질 때까지 대기하여 작업자 스레드를 안전하게 정리할 수 있습니다.


2
부울 플래그가 안전하지 않을 수 있습니다. 작업자가 작업을 완료하고 플래그가 읽힐 때까지 (읽을 경우) 범위를 유지한다는 것을 어떻게 보장합니까? 그것은 신호 작업입니다. 앨리어스 안전은 컴파일러가 (그리고 다른 모든 라이브러리 함수가) 플래그 변수의 상태를 변경할 수 있다고 가정하기 때문에 뮤텍스가 포함되지 않은 경우 휘발성은 간단한 스핀 락을 구현하는 데 좋습니다 mutex_lock.
Potatoswatter

6
분명히 작업자 스레드 루틴의 특성이 부울을 주기적으로 확인하도록 보장 된 경우에만 작동합니다. 휘발성 부울 플래그는 휘발성 부울을 보유한 객체가 소멸되기 전에 항상 스레드 종료 시퀀스가 ​​발생하고 스레드 종료 시퀀스는 bool을 설정 한 후 pthread_join ()을 호출하기 때문에 범위 내에서 유지됩니다. 작업자 스레드가 사라질 때까지 pthread_join ()이 차단됩니다. 신호는 특히 멀티 스레딩과 함께 사용될 때 자체 문제가 있습니다.
Jeremy Friesner

2
작업자 스레드 부울이 true가되기 전에 작업을 완료한다고 보장 할 수 없습니다 . 실제로 부울이 true로 설정되면 거의 확실하게 작업 단위의 중간에있게됩니다. 그러나 작업자 스레드가 종료 될 때까지 메인 스레드가 pthread_join () 내부에서 차단하는 것 외에는 아무것도하지 않기 때문에 작업자 스레드가 작업 단위를 완료하는 시점은 중요하지 않습니다. 따라서 종료 순서가 잘 정돈되어 있습니다. pthread_join ()이 반환 될 때까지 휘발성 부울 (및 기타 공유 데이터)이 해제되지 않고 작업자 스레드가 사라질 때까지 pthread_join ()이 반환되지 않습니다.
Jeremy Friesner

10
@ Jeeremy, 당신은 실제로 정확하지만 이론적으로 여전히 깨질 수 있습니다. 두 개의 코어 시스템에서 하나의 코어는 지속적으로 작업자 스레드를 실행합니다. 다른 코어는 bool을 true로 설정합니다. 그러나 작업자 스레드의 핵심이 변경 사항을 볼 수 있다는 보장은 없습니다. 즉, bool을 반복해서 확인하더라도 멈추지 않을 수 있습니다. 이 동작은 c ++ 0x, java 및 c # 메모리 모델에서 허용됩니다. 실제로 이것은 사용중인 스레드가 어딘가에 메모리 배리어를 삽입 할 때 발생하지 않으며, 그 후에는 bool의 변경을 볼 수 있습니다.
deft_code

4
POSIX 시스템을 사용하고, 실시간 스케줄링 정책을 사용하십시오. 시스템의 SCHED_FIFO다른 프로세스 / 스레드보다 높은 정적 우선 순위, 충분한 코어가 완벽하게 가능해야합니다. Linux에서는 실시간 프로세스가 CPU 시간의 100 %를 사용할 수 있도록 지정할 수 있습니다. 우선 순위가 높은 스레드 / 프로세스가없는 경우 컨텍스트 전환이 발생하지 않으며 I / O에 의해 차단되지 않습니다. 그러나 요점은 C / C ++ volatile가 적절한 데이터 공유 / 동기화 시맨틱을 시행하기위한 것이 아니라는 것입니다. 잘못된 코드가 때로는 작동 할 수 있음을 증명하기 위해 특별한 경우를 검색하는 것은 쓸모없는 운동입니다.
FooF

7

volatilespinlock 뮤텍스의 기본 구성을 구현하는 데 유용하지만 (충분하지는 않지만), 일단 그 (또는 우수한 것)이 있으면 다른 것이 필요하지 않습니다 volatile.

다중 스레드 프로그래밍의 일반적인 방법은 시스템 수준에서 모든 공유 변수를 보호하는 것이 아니라 프로그램 흐름을 안내하는 보호 변수를 도입하는 것입니다. volatile bool my_shared_flag;너 대신에

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

이것은 "하드 파트"를 캡슐화 할뿐만 아니라 기본적으로 필요합니다. C는 뮤텍스를 구현하는 데 필요한 원자 연산을 포함하지 않습니다 . 일반적인 작업 volatile에 대해서만 추가 보증을해야 합니다.

이제 다음과 같은 것이 있습니다.

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag 캐시 할 수없는 상태 임에도 불구하고 일시적 일 필요는 없습니다.

  1. 다른 스레드가 액세스 할 수 있습니다.
  2. 그것은 그것에 대한 참조가 언젠가 ( &운영자 와 함께) 취해 졌음을 의미합니다 .
    • (또는 함유 구조에 대한 참조가 취해졌습니다)
  3. pthread_mutex_lock 라이브러리 함수입니다.
  4. 컴파일러가 pthread_mutex_lock어떻게 든 그 참조를 얻었 는지 알 수 없다는 것을 의미합니다 .
  5. 컴파일러가 공유 플래그수정 한다고 가정 해야 함을 의미합니다 !pthread_mutex_lock
  6. 따라서 변수를 메모리에서 다시로드해야합니다. volatile이 문맥에서 의미가 있지만, 외부 적입니다.

6

당신의 이해는 실제로 잘못되었습니다.

휘발성 변수가 갖는 속성은 "이 변수에서 읽고 쓰는 것은 프로그램의 인식 가능한 동작의 일부"입니다. 이는이 프로그램이 작동 함을 의미합니다 (제공된 적절한 하드웨어)

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

문제는 이것이 스레드 안전에서 원하는 속성이 아니라는 것입니다.

예를 들어, 스레드 안전 카운터는 다음과 같습니다 (리눅스 커널과 유사한 코드, c ++ 0x에 해당하는 것을 모름).

atomic_t counter;

...
atomic_inc(&counter);

이것은 메모리 장벽이없는 원 자성입니다. 필요한 경우 추가해야합니다. 휘발성을 추가하면 주변 코드에 대한 액세스와 관련이 없기 때문에 도움이되지 않을 것입니다 (예 : 카운터가 계산하는 목록에 요소 추가). 물론, 프로그램 외부에서 카운터가 증가하는 것을 볼 필요는 없으며, 예를 들어 최적화가 여전히 바람직합니다.

atomic_inc(&counter);
atomic_inc(&counter);

여전히 최적화 할 수 있습니다

atomically {
  counter+=2;
}

옵티마이 저가 충분히 똑똑하다면 (코드의 의미를 변경하지 않습니다).


6

동시 환경에서 데이터가 일관성을 유지하려면 다음 두 가지 조건을 적용해야합니다.

1) 원 자성, 즉 메모리에서 일부 데이터를 읽거나 쓰면 해당 데이터가 한 번에 읽히고 쓰여지며 컨텍스트 스위치로 인해 중단되거나 문제를 일으킬 수 없습니다.

2) 일관성이 있어야합니다 읽기 / 쓰기 작전의 순서를, 즉 여러 개의 동시 환경과 동일하게 -하는 것이 스레드, 기계 등

휘발성은 상기 중 어느 것에도 맞지 않으며, 특히 휘발성이 어떻게 작용해야하는지에 관한 c 또는 c ++ 표준은 상기 중 어느 것도 포함하지 않습니다.

인텔 Itanium 컴파일러와 같은 일부 컴파일러는 동시 액세스 안전 동작의 일부 요소 (예 : 메모리 펜스를 보장)를 구현하려고 시도하지만 실제로는 구현이 더 나쁘지만 컴파일러 구현간에 일관성이 없으며 표준에는 이것을 요구하지 않습니다. 우선 구현의.

변수를 휘발성으로 표시하면 기본적으로 캐시 성능이 저하되어 코드가 느려질 때마다 값을 메모리로 플러시하거나 메모리에서 플러시해야합니다.

c # 및 java AFAIK는 1) 및 2)를 휘발성으로 준수 하여이 문제를 해결하지만 c / c ++ 컴파일러에 대해서도 동일하게 말할 수 없으므로 기본적으로 적합하다고 생각하면됩니다.

주제에 좀 더 깊이 (편견 아니지만) 설명은 읽는 이를


3
+1-보장 된 원자 성은 내가 잃어버린 것의 또 다른 조각이었습니다. 나는 int를로드하는 것이 원자 적이라고 가정하여 리오 더링을 방지하는 휘발성은 읽기 측에 완전한 솔루션을 제공했습니다. 대부분의 아키텍처에서 적절한 가정이라고 생각하지만 보장하지는 않습니다.
Michael Ekstrand

메모리에 대한 개별 읽기 및 쓰기는 언제 인터럽트 가능하고 비 원자입니까? 어떤 이점이 있습니까?
batbrat

5

comp.programming.threads FAQ는 Dave Butenhof 의 고전적인 설명입니다 .

Q56 : 공유 변수를 VOLATILE로 선언 할 필요가없는 이유는 무엇입니까?

그러나 컴파일러와 스레드 라이브러리가 각각의 사양을 충족시키는 경우에 대해 걱정하고 있습니다. 적합한 C 컴파일러는 일부 공유 (비 휘발성) 변수를 CPU가 스레드에서 스레드로 전달 될 때 저장 및 복원되는 레지스터에 전역 적으로 할당 할 수 있습니다. 각 스레드는이 공유 변수에 대한 자체 값을 가지게되는데 이는 공유 변수에서 원하는 것이 아닙니다.

어떤 의미에서 컴파일러가 변수의 각 범위와 pthread_cond_wait (또는 pthread_mutex_lock) 함수에 대해 충분히 알고 있다면 이것은 사실입니다. 실제로 대부분의 컴파일러는 루틴이 어떻게 든 데이터 주소에 액세스 할 수 있는지 여부를 알기가 어렵 기 때문에 외부 함수 호출을 통해 전역 데이터의 복사본을 유지하려고 시도하지 않습니다.

따라서 ANSI C를 엄격하게 (매우 적극적으로) 준수하는 컴파일러는 휘발성이없는 여러 스레드에서 작동하지 않을 수 있습니다. 그러나 누군가가 더 잘 고쳤습니다. POSIX 메모리 일관성 보장을 제공하지 않는 SYSTEM (즉, 커널, 라이브러리 및 C 컴파일러의 조합)은 POSIX 표준을 따르지 않기 때문입니다. 기간. POSIX는 POSIX 동기화 기능 만 필요하기 때문에 시스템은 올바른 동작을 위해 공유 변수에 휘발성을 사용할 필요가 없습니다.

따라서 휘발성을 사용하지 않아 프로그램이 중단되면 버그입니다. C의 버그, 스레드 라이브러리의 버그 또는 커널의 버그가 아닐 수 있습니다. 그러나 이것은 SYSTEM 버그이며 이러한 구성 요소 중 하나 이상이 문제를 해결해야합니다.

차이를 만드는 시스템에서는 적절한 비 휘발성 변수보다 훨씬 비싸기 때문에 휘발성을 사용하고 싶지 않습니다. (ANSI C는 각 표현식에서 휘발성 변수에 대해 "시퀀스 포인트"가 필요한 반면 POSIX는 동기화 작업에서만 변수를 필요로합니다. 계산 집약적 인 스레드 응용 프로그램은 휘발성을 사용하여 실질적으로 더 많은 메모리 활동을 볼 수 있으며 결국 메모리 활동입니다. 정말 속도가 느려집니다.)

/ --- [Dave Butenhof] ----------------------- [butenhof@zko.dec.com] --- \
| Digital Equipment Corporation 110 스핏 브룩로드 ZKO2-3 / Q18 |
| 603.881.2218, 팩스 603.881.0120 Nashua NH 03062-2698 |
----------------- [동시성을 통한 더 나은 생활] ---------------- /

Butenhof는 이 유즈넷 게시물 에서 동일한 근거를 다룹니다 .

"휘발성"을 사용하는 것은 적절한 메모리 가시성 또는 스레드 간 동기화를 보장하기에 충분하지 않습니다. 뮤텍스의 사용은 충분하며, 이식 불가능한 다양한 머신 코드 대안 (또는 이전 게시물에서 설명한 것처럼 일반적으로 적용하기가 훨씬 어려운 POSIX 메모리 규칙의 미묘한 영향)을 제외하고는 충분하지 않습니다. 뮤텍스는 필수입니다.

따라서 Bryan이 설명했듯이 휘발성을 사용하면 컴파일러가 유용하고 바람직한 최적화를 수행하지 못하도록 방지 할 수 있으며 코드 "스레드 안전"을 만드는 데 도움이되지 않습니다. 물론 원하는 것을 "휘발성"으로 선언 할 수 있습니다. 이는 결국 유효한 ANSI C 스토리지 속성입니다. 스레드 동기화 문제를 해결할 것으로 기대하지 마십시오.

C ++에도 똑같이 적용됩니다.


링크가 끊어졌습니다. 더 이상 당신이 인용하고 싶은 것을 가리 키지 않는 것 같습니다. 텍스트가 없으면 의미없는 대답이됩니다.
jww

3

"불안정한"작업이 전부입니다. "이 컴파일러는 로컬 명령이 실행되지 않더라도이 변수가 모든 시계 틱에서 변경 될 수 있습니다.이 값을 레지스터에 캐시하지 마십시오."

바로 IT입니다. 컴파일러에게 값이 휘발성이라는 것을 알려줍니다.이 값은 외부 로직 (다른 스레드, 다른 프로세스, 커널 등)에 의해 언제든지 변경 될 수 있습니다. 캐시에 본질적으로 안전하지 않은 레지스터의 값을 자동으로 캐시하는 컴파일러 최적화를 억제하는 것은 전적으로 존재합니다.

멀티 스레드 프로그래밍을위한 만병 통치약으로 변덕스러운 "Dr. Dobbs"와 같은 기사가 나타날 수 있습니다. 그의 접근 방식에는 완전히 장점이 없지만 객체 사용자가 스레드 안전을 책임지게하는 근본적인 결함이 있습니다. 이는 다른 캡슐화 위반과 동일한 문제를 일으키는 경향이 있습니다.


3

저의 오래된 C 표준에 따르면, "유효한 유형을 가진 객체에 대한 액세스를 구성하는 것은 구현 정의되어 있습니다" . 따라서 C 컴파일러 작성자 는 " 다중 프로세스 환경에서 스레드 안전 액세스"를 "휘발성"으로 선택했을 있습니다 . 그러나 그들은하지 않았다.

대신, 멀티 코어 멀티 프로세스 공유 메모리 환경에서 중요한 섹션 스레드를 안전하게 만드는 데 필요한 작업이 새로운 구현 정의 기능으로 추가되었습니다. 그리고 "휘발성"이 다중 프로세스 환경에서 원자 적 액세스 및 액세스 순서를 제공해야한다는 요구에서 벗어난 컴파일러 작성자는 과거의 구현 의존적 "휘발성"시맨틱보다 코드 축소를 우선시했습니다.

즉, 새로운 컴파일러가있는 새 하드웨어에서는 작동하지 않는 중요 코드 섹션 주위의 "휘발성"세마포어와 같은 것들이 옛날 하드웨어의 오래된 컴파일러에서는 한 번 작동했을 수도 있고, 오래된 예제는 때때로 오래된 것이 아닙니다.


이전 예제에서는 프로그램을 저수준 프로그래밍에 적합한 고품질 컴파일러로 처리해야했습니다. 불행히도, "현대"컴파일러는 표준에 따라 "휘발성"을 유용한 방식으로 처리 할 필요가 없다는 사실을 받아 들였다. 적합하지만 품질이 좋지 않은 구현을 금지하려는 노력을했지만, 인기를
얻지 못한 저품질

대부분의 플랫폼에서 volatile하드웨어에 의존하지만 컴파일러에 독립적 인 방식으로 OS를 작성하기 위해 수행해야 할 작업 을 인식하기가 매우 쉽습니다 . 프로그래머가 필요에 따라 volatile작업 을 수행 하기보다는 구현에 따른 기능을 사용하도록 요구 하면 표준을 갖는 목적이 손상됩니다.
supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.