KoreanFoodie's Study

Effective Modern C++ | 항목 40 : 동시성에는 std::atomic 을 사용하고, volatile 은 특별한 메모리에 사용하라. 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 40 : 동시성에는 std::atomic 을 사용하고, volatile 은 특별한 메모리에 사용하라.

GoldGiver 2022. 10. 26. 10:09

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 40 : 동시성에는 std::atomic 을 사용하고, volatile 은 특별한 메모리에 사용하라.

핵심 :

1. std::atomic 은 뮤텍스 보호 없이 여러 스레드가 접근하는 자료를 위한 것으로, 동시적 소프트웨어의 작성을 위한 도구이다.
2. volatile 은 읽기와 기록을 최적화로 제거하지 말아야 하는 메모리를 위한 것으로, 특별한 메모리를 다룰 때 필요한 도구이다.

 

std::atomic 은 보통 뮤텍스보다 더 효율적인 기계어 명령들로 구현된다. std::atomic 을 사용하는 다음 예제 코드를 보자.

std::atomic<int> ai(0);

ai = 10;

std::cout << ai;

++ai;
--ai;

우리는 두 가지를 주목해야 한다.

첫째, "std::cout << ai" 에서, std::atomic 객체가 보장하는 것은 ai 의 읽기가 원자적이라는 것뿐이다. ai 의 값을 읽는 시점과 operator<< 가 호출되는 시점 사이에 다른 스레드가 ai 의 값을 수정할 수는 있다.

둘째, 마지막 두 문장처럼 증가/감소 연산은 각각 원자적으로 수행된다.

 

다만 volatile 을 사용하는 다음 코드는 다중 스레드 문맥에서 거의 아무것도 보장하지 않는다.

volatile int vi(0);

vi = 10;

std::cout << vi;

++vi;
--vi;

이 코드를 실행하는 동안 vi 의 값을 다른 스레드들이 읽는다면, 그 스레드들은 어떤 값이라도 볼 수 있다. 이런 코드는 미정의 행동을 유발한다. 그리고 여러 스레드에서 증가 연산(++) 를 했는데, 실제로는 딱 한 번만 증가하는 기이한 현상도 생길 수 있다. 메모리에 기록자(writer)들과 판독자(reader) 들이 동시에 접근하려 해서 자료 경쟁(data race) 가 일어나기 때문이다.

또한, std::atomic 의 경우는 코드의 순서 재배치에 대한 제약을 부여한다. 그런 제약 중 하나는, std::atomic 변수를 기록하는 문장 이전에 나온 그 어떤 코드도 그 문장 이후에 실행되지 않아야 한다는 것이다. 그런데 volatile 은 그런 제약이 없으므로, 항목 39 에서처럼 '통지를 보내는' 코드의 경우, 실제 검출이 일어나는 코드가 통지를 보내는 코드 이후로 재배치될 수도 있다.

 

그렇다면 volatile 은 언제 사용해야 할까? 다음과 같은 코드를 보자.

int x;
 
auto y = x;
y = x;

x = 10;
x = 20;

위의 코드에서, "y = x " 문과 "x = 10" 이 불필요해 보이지만, 사실 이게 특별한 명령일 수도 있다. 이런 경우, x 를 volatile 로 만들어 주어야 한다!

// 이후 코드를 임의로 최적화 하지 않기!
volatile int x;

auto 는 const 와 volatile 한정사가 제거되므로 그냥 int 가 될 것이다.

만약 x 를 "std::atomic<int> x;" 로 바꾸면, 컴파일이 실패한다. "y = x" 문장이 실패하기 때문인데, 이는 std::atomic 의 복사 연산들이 삭제되었기 때문이다(복사 생성과 복사 배정 둘 다). 대신 다음과 같이 수정하면 컴파일이 된다.

std::atomic<int> x;
 
std::atomic<int> y(x.load());
y.store(x.load());

load 과 store 은 원자적인 연산이지만, 두 문장이 각자 하나의 원자적 연산으로 실행되리라고 기대할 수는 없다. 컴파일러는 x 값을 레지스터에 저장해 이러한 코드를 '최적화' 할 수도 있다.

레지스터 = x.load();
 
std::atomic<int> y(레지스터);
y.store(레지스터);

 

즉, std::atomic 과 volatile 은 용도가 다르므로, 함께 사용하는 것도 가능하다.

volatile std::atomic<int> vai;

이 코드는 vai 가 여러 스레드가 동시에 접근할 수 있는 메모리 대응 입출력 장소일때 유용할 것이다.

Comments