35. volatile 의 의미와 오해 (MMIO/신호용이지 스레드 동기화가 아니다)
난이도 중내 리뷰 · C++
해설 · C++
해설 — volatile 의 의미와 오해 (MMIO/신호용이지 스레드 동기화가 아니다)
난이도: 중상
요약
volatile은 "이 변수에 대한 접근을 컴파일러가 생략/합치지 말고 소스에 적힌 그대로
매번 수행하라"는 지시일 뿐이다. 원자성도, 스레드 간 가시성 순서(happens-before)도
보장하지 않는다. 실측에서 두 스레드가 vcounter를 각 200만 번 증가시켰지만 결과는
기대치 400만이 아니라 ~210만이다(경쟁으로 갱신 유실). 반면 std::atomic은 정확히
400만이다. 스레드 간 공유 상태는 std::atomic 또는 뮤텍스로 다뤄야 한다.
실측 결과
expected = 4000000
volatile got = 2083120 (실행마다 다름, 항상 < 4000000)
atomic got = 4000000 (항상 정확)
핵심 개념: volatile 이 실제로 보장하는 것
- 접근 생략 금지:
while (flag) {}에서flag가 non-volatile이면 컴파일러는 "루프 안에서 안 바뀐다"고 보고 레지스터에 캐싱해 무한 루프로 만들 수 있다.volatile이면 매 반복 메모리를 다시 읽는다. - 접근 합치기/재배열(단일 스레드 내 관찰 가능 부작용) 금지: volatile 접근끼리는 프로그램 순서가 유지되고, 개수도 보존된다.
- 원래 용도: 메모리 맵 I/O 레지스터(MMIO), 시그널 핸들러와 공유하는
volatile sig_atomic_t,setjmp/longjmp로 살아남아야 하는 지역 변수. 즉 "컴파일러가 모르는 이유로 메모리가 바뀌거나, 읽기/쓰기 자체가 부작용인" 경우.
volatile 이 보장하지 않는 것 (오해의 핵심)
- 원자성 없음:
vcounter = vcounter + 1은 load → add → store 세 기계 명령으로 컴파일된다. volatile은 각 접근을 "진짜로 수행"하게 할 뿐, 세 명령을 불가분으로 묶지 않는다. 두 스레드가 같은 시점에 load하면 한쪽의 store가 다른 쪽에 덮여 갱신이 유실된다(실측의 ~190만 손실). - 메모리 순서/가시성 없음: volatile 접근은 non-volatile 메모리 접근이나 다른 스레드의 관점과 순서 관계를 만들지 않는다. happens-before가 없으므로, volatile 플래그로 "데이터 준비됨"을 알린 뒤 데이터를 읽는 패턴은 CPU/컴파일러 재배열로 깨질 수 있다(플래그는 보였는데 데이터는 옛 값).
- CPU 캐시 강제 플러시 아님: volatile은 캐시 일관성 프로토콜이나 메모리 배리어를 삽입하지 않는다. 하드웨어 수준 가시성은 여전히 미보장.
C++ 의 올바른 도구
std::atomic<long> counter{0};
counter.fetch_add(1); // 원자적 RMW
std::atomic<bool> ready{false};
// 생산자: 데이터 쓰고 ready.store(true, release);
// 소비자: if (ready.load(acquire)) 데이터 읽기; // release/acquire 로 happens-before 성립
std::atomic은 (a) 원자적 RMW, (b)memory_order로 가시성/순서(happens-before), (c) 필요한 배리어를 함께 제공한다. 이것이 volatile과의 결정적 차이다.- 복합 불변식(여러 변수의 일관된 갱신)은 atomic으로도 부족하고 뮤텍스가 필요하다.
흔한 오해·함정
- "volatile 이면 스레드 안전" → 틀림. Java/C# 의
volatile과 혼동한 것. C#의volatile은 acquire/release 의미를 갖지만 C/C++ 의 volatile은 스레드 의미가 전혀 없다. 언어마다 정의가 다르다. - "플래그 하나면 원자성 필요 없잖아?" → 단일 바이트 정렬 접근이 찢기지 않더라도, 순서/가시성 보장이 없으면 그 플래그로 보호하려던 데이터가 재배열로 새어나간다.
- "성능 위해 atomic 대신 volatile" → atomic relaxed는 volatile과 코드젠이 거의 같으면서 원자성을 보장한다. volatile로 얻는 성능 이득은 정확성을 버린 대가일 뿐.
std::atomic<T>와volatile std::atomic<T>는 다르다(후자는 MMIO+원자 조합의 드문 경우).
면접 포인트
- "volatile 은 언제 쓰나?" → MMIO 레지스터, 시그널 핸들러 공유(
sig_atomic_t),longjmp생존 변수. 스레드 동기화에는 쓰지 않는다. - "volatile 과 atomic 의 차이?" → volatile = 접근 생략/합치기 금지(부작용 보존). atomic = 원자성 + 메모리 순서(happens-before) + 배리어. 스레드 공유는 atomic/뮤텍스.
- "C#의 volatile 과 같나?" → 아니다. C#/Java volatile은 메모리 순서 의미가 있지만 C++ volatile은 없다. 언어별 정의를 반드시 구분하라.