KoreanFoodie's Study
Effective Modern C++ | 항목 16 : const 멤버 함수를 스레드에 안전하게 작성하라 본문
Effective Modern C++ | 항목 16 : const 멤버 함수를 스레드에 안전하게 작성하라
GoldGiver 2022. 10. 26. 09:54
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 16 : const 멤버 함수를 스레드에 안전하게 작성하라
핵심 :
1. 동시적 문맥에서 쓰이지 않을 것이 확실한 경우가 아니라면, const 멤버 함수는 스레드에 안전하게 작성하라.
2. std::atomic 변수는 mutex 에 비해 성능상의 이점이 있지만, 하나의 변수 또는 메모리 장소를 다룰 때에만 적합하다.
다음과 같은 다항식 클래스가 있다고 하자.
class Polynomial {
public:
using RootsType =
std::vector<double>;
RootsType roots() const {
// 캐시가 유효하지 않으면
if (!rootsAreValid) {
// 근들을 계산해서 rootVals 에 저장
...
rootsAreValid = true;
}
return rootVals;
}
private:
mutable bool rootsAreValid{ false; }
mutable RootsType rootVals{}
};
다항식이 0으로 평가되는 값들을 계산하는 멤버 함수는 다항식을 수정하지 않음으로, 위와 같이 const 로 선언하는 것이 바람직하다. 또한, 다항식 근의 계산 비용은 클 수 있으므로, 꼭 필요할 때만 계산하는 것이 바람직하다.
roots 는 개념적으로는 자신이 속한 Polynomial 객체를 변경하지 않는다. 그러나 캐싱을 위해서는 rootVal 과 rootsAreValid 의 변경이 필요할 수 있다(그래서 mutable 을 사용함). 즉, 여러 스레드가 roots() 를 동시에 호출하면 경쟁 상황(race condition) 이 발생할 수 있다.
이를 해결하는 가장 쉬운 방법은 통상적인 동기화 해결책인 뮤텍스를 사용하는 것이다.
class Polynomial {
public:
using RootsType =
std::vector<double>;
RootsType roots() const {
// 뮤텍스를 잠근다
std::lock_guard<std::mutex> g(m);
... // 기존과 동일
return rootVals;
// 뮤텍스를 푼다
}
private:
mutable std::mutex m;
...
};
std::mutex 는 복사하거나 이동할 수 없으므로, m 을 Polynomial 에 추가하면 Polynomial 의 복사와 이동 능력도 사라진다.
그리고 이런 목적으로 뮤텍스를 도입하는 것이 과한 일일 수 있다. 함수 호출 횟수를 세는 간단한 경우에는 std::atomic 을 사용하는 것이 효율적일 수도 있다.
class Point {
public:
void f() {
++callNums;
// Do something...
}
private:
mutable std::atomic<int> callCount{ 0 };
};
std::atomic 도 복사와 이동이 불가능하다. 또한, std::atomic 의 남용은 자제해야 한다. 다음과 같은 예시를 보자.
class Widget {
public:
int magicValue() const
{
if (cacheValid) {
return cachedValue;
} else {
auto val1 = expensiveComputation();
auto val2 = expensiveComputation();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
}
int expensiveComputation() { /* ...expensive! */}
private:
mutable std::atomic<bool> cacheValid{ false };
mutable std::atomic<int> cachedValue;
};
여러 개의 스레드가 magicValue 를 호출한다고 가정해 보자. 만약 cacheValid 를 갱신하기 이전에, 여러 스레드가 cachedValue 를 계산하는 부분까지만 실행을 하면, 불필요한 계산이 매우 많이 일어나게 될 것이다.
심지어 cachedValue 와 cacheValid 를 건드리는 코드의 순서를 바꾸면, 문제가 더 심각해질 수 있다. cacheValid 를 true 로 바꾼 상태에서 한 스레드가 멈춘 후, 다른 스레드가 실행되면 정확하지 않은 cachedValue 값을 읽을 수 있기 때문이다.
즉, 동기화가 필요한 변수 하나 또는 메모리 장소 하나에 대해서는 std::atomic 을 쓰는 것이 적합하지만, 둘 이상일 경우, 뮤텍스를 사용하는 것이 바람직하다. 이 항목에서 강조한 것처럼, const 멤버 함수가 언제라도 동시적 실행 상황에 처할 것이라고 가정하는 것이 안전하며, const 멤버 함수는 항상 스레드에 안전하게 만들어야 한다.
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective Modern C++ | 항목 18 : 소유권 독점 자원의 관리에는 std::unique_ptr 를 사용하라 (0) | 2022.10.26 |
---|---|
Effective Modern C++ | 항목 17 : 특수 멤버 함수들의 자동 작성 조건을 숙지하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 15 : 가능하면 항상 constexpr 를 사용하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 14 : 예외를 방출하지 않을 함수는 noexcept 로 선언하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 13 : iterator 보다 const_iterator 를 선호하라 (0) | 2022.10.26 |