본문 바로가기

Library/C/C++

volatile Qualifier and Memory Barrier

멀티스레드 상황에서, 메모리 쓰기 순서를 유지하여 오동작을 막아준다고 알려져왔던 이중 검사 동기화 패턴(double checked locking)은, 안타깝지만 모든 상황에서 제대로 동작하는 것이 아니다. CPU 런타임 시점에 이루어지는 실행 순서 재배열 문제 때문인데, 한층 더 불행한 소식은 이 문제를 방지하기 위한 volatile 한정자 선언조차 여기서 큰 도움이 되지 않는다는 점이다.

C 표준에서는, volatile로 선언된 변수일 경우, 그 실행 순서가 도달하기 전까지 지정했던 동작의 완료를 보장하도록 한다. 즉, memory-mapped IO와 같은 기법에서 유용하게 쓰이며, 실제로도 그런 목적으로 고안된 것이다. 그러나, C++에서는 약간의 모호함이 있다. 즉, 실제로 volatile 선언은 계속해서 참조되는 메모리에 위치한 변수를 레지스터로 옮겨 메모리 접근 시간을 줄이는 것과 같은 최적화를 하지 않도록 컴파일러에게 지시하는 것이다. 따라서, 어느 순간에 메모리에 있는 값이 수정될지 모르는 멀티스레드 상황에서 이런 최적화를 금지하는 것은 유용해보인다. 하지만, 불행하게도 이런 메모리 장벽과 같은 효과를 기대하고 volatile을 사용할 수 없다. 컴파일러가 항상 값을 메모리에서 얻어오도록 코드를 만들고, 실행 순서의 재배열을 허용하지 않는다고 할지라도 그것만으로 메모리 장벽 효과가 발생하지 않는다.

컴파일러가 안전한 코드를 생성하더라도, 이와 별도로 CPU 내부에서도 실행 순서를 재배열하기 때문에, memory-mapped IO 기법에서는 큰 문제가 되지 않을지 몰라도 멀티스레드 상황에서 단지 volatile 한정자를 선언하는 것만으로는 안전한 코드를 작성할 수 없다. 위에서 말한 이중 검사 동기화 패턴을 싱글턴에 적용할 경우, 특정 기계에서는 멀티스레드 상황에서 실행 순서가 뒤바뀌면 이미 검사를 통과한 스레드가 쓰레기값을 얻으면서 비극적인 동작을 할 가능성이 있기 때문이다. 결론적으로 말하면 이렇다.


"멀티스레드에서 사용되는 변수를 방어할 목적으로 volatile 한정자를 사용하는 것은 완전한 해결책은 아니다. 특히, C++ 표준은 volatile 한정자 구현을 컴파일러에게 맡겨두고 있다. 경우에 따라서는 도움이 될 수도 있지만(Visual C++), 그런 코드는 이식성을 보장할 수 없다. volatile 한정자는 중요한 최적화를 하지 않는 만큼 성능에서 크게 손해 볼 수도 있으므로, 신중하게 사용해야 한다."