KoreanFoodie's Study
Effective Modern C++ | 항목 40 : 동시성에는 std::atomic 을 사용하고, volatile 은 특별한 메모리에 사용하라. 본문
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 가 여러 스레드가 동시에 접근할 수 있는 메모리 대응 입출력 장소일때 유용할 것이다.
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective Modern C++ | 항목 42 : 삽입 대신 생성 삽입을 고려하라 (0) | 2022.10.26 |
---|---|
Effective Modern C++ | 항목 41 : 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 39 : 단발성 사건 통신에는 void 미래 객체를 고려하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 38 : 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 37 : std::thread 들을 모든 경로에서 합류 불가능하게 만들어라 (0) | 2022.10.26 |