다항식의 근을 구하는 등 계산 비용이 큰 함수들은 꼭 필요할 때에만 계산하는 것이 바람직하다. 또한 중복해서 계산하는 것은 피해야 하며, 필요한 때만 근을 계산해서 캐시에 저장하고 그렇지 않을 때에는 캐시에 있는 값을 돌려주도록 함수를 구현하는 것이 좋다.
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{};
};
roots는 개념적으로 자신이 속한 Ploynomial의 객체를 변경하지 않지만 캐싱을 위해 rootVals와 rootsAreValid의 변경이 필요할 수 있다. 그렇기 때문에 mutable로 선언했다.
하지만 만약 두 스레드가 동시에 하나의 Polynomial 객체에 대해 roots를 호출한다고 가정해보자.
개념적으로 여러 스레드에서 동기화 없이 읽기 연산을 수행하는 것은 안전하다. 하지만 roots는 const의 멤버함수이긴 하지만 함수 내에서 자료멤버 수정을 시도 할 수 있다. 이러한 경우 data race 상황이 일어나며, 이 코드는 미정의 행동을 유발할 수 있다.
통상적으로 mutex를 사용해 이를 해결 할 수 있다. 하지만 이 경우, std::mutex를 복사하거나 이동할 수 없기 때문에 std::mutex를 추가 하면, Polynomial의 복사, 이동능력이 사라지게 된다.
class Polynomial{
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
std::lock_guard<std::mutex> g(m); // 뮤텍스를 잠근다
if (!rootsAreValid){ // 캐시가 유효하지 않으면 근들을 계산해서 rootVals에 저장
...
rootsAreValid = true;
}
return rootVals;
} // 뮤텍스를 푼다
private:
mutable std::mutex m;
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};
std::mutex의 형식은 mutable로 선언되었는데, m을 잠그고 푸는 멤버함수들은 const 형식이 아니지만 const 멤버 함수인 roots 안에서는 m이 const 객체로 간주되기 때문에 이렇게 해야 한다.
또 하나의 예로 계산 비용이 큰 int 값을 캐시에 저장하는 클래스에서는 한 쌍의 std::atomic 변수를 사용하는 것을 생각 해 볼 수 있다.
class Widget {
public:
...
int magicValue() const
{
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
}
private:
mutable std::atomic<bool> cacheValid{ false };
mutable std::atomic<int> cachedValue;
};
만약 한 스레드가 비용이 큰 계산을 끝내고 둘의 합을 cachedValue에 배정할 때 두번째 스레드가 magicValue()를 호출하는 경우 앞의 스레드와 동일한 비용이 큰 계산을 한다.
cachedValue와 cacheValid의 순서를 바꾼 경우에, 한 스레드가 cacheValid를 true로 바꿔준 뒤 cachedValue를 배정하는 시점에서 두 번째 스레드가 magicValue()를 호출하는 경우 계산되지 않은 cachedValue를 갖게 된다.
이처럼 동기화에 필요한 변수가 둘 이상의 변수나 메모리 장소를 하나의 단위로 조작해야 할 때는 뮤텍스를 사용하는 것이 바람직하다.
Conclusion
- 동시적 문맥에서 쓰이지 않을 것이 확실한 경우가 아니라면, const 멤버함수는 항상 thread-safe하게 작성하라
- std::atomic 변수는 뮤텍스에 비해 성능상의 이점이 있지만, 하나의 변수 또는 메모리 장소를 다룰 때에만 적합하다.
Reference
Effective Modern C++ 항목 16: const 멤버 함수를 스레드에 안전하게 작성하라
'C++ > Effective Modern C++' 카테고리의 다른 글
std::unique_ptr (0) | 2020.07.12 |
---|---|
특수 멤버 함수들의 자동 작성 조건을 숙지하라 (0) | 2020.07.10 |
가능하면 항상 constexpr을 사용하라 (0) | 2020.07.08 |
예외를 방출하지 않을 함수는 noexpect로 (0) | 2020.07.07 |
iterator보다는 const_iterator를 (0) | 2020.07.06 |
댓글