프로그래머들은 종종 volatile 과 std::atomic 을 혼동한다. 그러나 이 두 개는 서로 전혀 다른 역할을 한다. std::atomic 은 원자성을 보존해주기 위해서 사용된다. 다시 말하자면 mutex 없이도 두 개의 스레드가 동시에 접근해도 data race 문제가 발생하지 않는다. volatile 은 컴파일러가 최적화하며 특정 라인을 수행하지 않는 경우가 없도록 해준다. 코드로 살펴보자.
volatile int vi(0);
++vi;
--vi;
std::cout << vi;
위 코드의 출력 결과는 당연히 0이다. 값은 초기와 다르지 않다. 우리가 생각하기에도 무의미한 계산을 한 것이다. 컴파일러는 최적화를 하며 두 개의 라인을 무시할 수 있다. 그런데 만약 임베디드나 특정한 상황에서 저 계산식은 무시가 되면 안되는 경우가 발생한다. 그런 경우에 사용하는 것이 volatile 이다. 컴파일러가 무시할 가능성을 없애주는 것이다. 그렇다고해서 data race 문제는 해결되지 않는다. 두 개의 스레드가 동시에 접근은 가능하다는 의미이다. 반대로 std::atomic 에서는 무의미한 계산을 무시할 수 있다.
코드 재배치
또다른 차이점으로는 코드 재배치라는 컴파일러 최적화가 있다. 컴파일러는 코드 라인의 순서를 임의대로 바꿀 수 있다. 레지스터에 적재하고 내리는데 사용하는 시간을 최적화하기 위해서 말이다. 그런데 std::atomic 에서는 이야기가 달라진다. std::atomic 을 기록하는 라인이 수행될 때는 그 이전에 나타난 라인들이 수행되어서는 안된다. 이것은 std::atomic 의 특성이다. 다시 말하자면 volatile 은 그럴 필요가 없다는 것이다. 코드 재배치로 라인이 변경될 수 있다.
std::atomic
좀 더 std::atomic 의 특징들을 살펴보자. std::atomic 은 복사와 이동 연산을 지원하지 않는다. 그 이유는 간단하다. 원자성을 보존해야 하는게 주 목적인데 복사를 위해 읽고 기록하는 작업을 원자적으로 계산하는 하드웨어는 지원하지 않기 때문이다. 다행히 복사를 하는 방법이 가능하다.
std::atomic<int> y(x.load());
y.store(x.load());
레지스터를 이용하여 최적화도 가능한데 C++17 부터는 의미가 없기 때문에 넘어가도록 하겠다.
결론
우리는 std::atomic 과 volatile 의 차이점에 대해서 알아보았다. 이 두 개는 서로 전혀 다른 역할을 한다는 것을 우리는 알 수 있었다. 따라서 두 개는 한꺼번에 사용도 가능하다.
volatile std::atomic<int> val;
'개발 🛠💻 > C++, Modern C++' 카테고리의 다른 글
C++17 std::optional (0) | 2023.09.08 |
---|---|
[전문가를 위한 C++] 11장 11.1.6 레퍼런스와 포인터의 선택 기준 (0) | 2023.04.09 |
STL 컨테이너별 시간 복잡도 (0) | 2022.08.05 |
[C++ 17] Fold Expressions (0) | 2022.08.05 |
C++ 특수 멤버 함수와 The rule of three five zero (3/5/0) (0) | 2022.07.25 |
댓글