일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
Tags
- 상속
- iterator
- 반복자
- 티스토리챌린지
- 영화 리뷰
- 오블완
- universal reference
- 예외
- virtual function
- resource management class
- UE4
- more effective c++
- 게임
- operator new
- effective modern c++
- implicit conversion
- 언리얼
- 보편 참조
- exception
- std::async
- Effective c++
- c++
- 스마트 포인터
- 암시적 변환
- lua
- 참조자
- reference
- 영화
- Smart Pointer
- effective stl
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 16. thread safety한 const 멤버 함수 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 16. thread safety한 const 멤버 함수
김디트 2025. 2. 26. 12:27728x90
항목 16. const 멤버 함수를 스레드에 안전하게 작성하라
const 함수를 가지는 다항식(polynomial) 예제
class Polynomial
{
public:
// 다항식의 근들
// 즉, 다항식이 0으로 평가되는 값들
using RootsType = std::vector<double>;
RootsType roots() const // 캐시된 값을 그저 리턴
{
if(!rootsAreValid)
{
... // 근들을 계산해서 rootVals에 캐싱
rootsAreValid = true;
}
return rootVals;
}
private:
mutable bool rootsAreValid { false }; // 중괄호 초기지(항목 7 참조)
mutable RootsType rootVals{}; // 중괄호 초기지(항목 7 참조)
};
- roots 함수는 기본적으로 개체를 수정하지 않는다.
- 헌데 캐싱을 위해선 rootVals나 rootsAreValid의 변경이 필요하다.
- 그러므로 변수들을 mutable을 적용하였다.(const 함수 내에서도 변경 가능)
- 근데 두 쓰레드에서 한 객체에 대해 동시에 roots를 호출한다면 어떻게 될까?
- 이 코드는 합법하다.
- roots는 const 멤버 함수이므로 읽기 전용이라는 뜻이다.
- 여러 스레드가 동기화 없이 읽기를 수행하는 건 안전하니까.
- 허나 위의 예에서는 코드가 안전하지 않다.
- mutable 내부 변수를 수정하려 들기 때문이다.
- data race 상태에 이를 수 있다.
- 즉, 미정의 행동을 유발하는 코드이다.
뮤텍스를 사용해서 해결?
class Polymial
{
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
std::lock_guard<std::mutex> g(m);
if(!rootsAreVaild)
{
...
rootsAreValid = true;
}
return rootsVals;
}
private:
mutable std::mutex m ;
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};
- Polynomial의 복사와 이동이 불가해진다.
- 멤버 변수 m 때문이다.
- 뮤텍스는 복사하거나 이동할 수 없다.
- 뮤텍스를 도입하는 건 너무 과한 일이다.
- 비용이 너무 커진다.
std::atomic을 사용하는 예제
- 뮤텍스는 비용이 크니까 비용이 작은 atomic을 사용해보자는 아이디어.
- 그 아이디어를 적용한 또 다른 예제
class Point
{
public:
...
double distanceFromOrigin() const noexcept
{
++callCount;
return std::hypot(x, y); // C++11에 추가된 함수
}
private:
mutable std::atomic<unsigned> callCount{ 0 }; // 아토믹 사용
double x, y;
};
- Point 역시 복사와 이동이 불가능해진다.
- std::atomic도 복사와 이동이 불가능하기 때문이다.
- 허나 std::atomic이 뮤텍스보다 비용이 싸다는 점에 현혹되어 함정에 빠질 수 있다.
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;
};
- 아래와 같은 시나리오를 생각해보자.
- 한 스레드가 Widget::magicValue를 호출한다.
- cacheValid가 false임을 확인한다.
- 비용이 큰 두 계산을 수행한 후 둘의 합을 cachedValue에 할당한다.
- 이 때 다른 스레드가 Widget::magicValue를 호출한다.
- cacheValid가 false임을 확인한다.
- 비용이 큰 두 계산을 재수행한다.
- 근데 다른 스레드는 한개가 아닐 수 있다.
- cacheValid를 먼저 수행하면 해결이 될까?
- cacheValid가 true로 설정되기 전에 비용이 큰 계산을 수행하게 되므로 문제 해결에 도움이 되지 않는다.
- 게다가 더 안 좋은 시나리오가 발생할 수도 있다!!
- cacheValid를 먼저 수행하는 시나리오를 생각해보자.
- 한 스레드가 Widget::magicValue를 호출한다.
- cacheValid에 true가 할당된다.
- 그 시점에 둘째 스레드가 Widget::magicValue를 호출한다.
- cacheValid를 점검해서 true임을 확인한다.
- 할당되지 않은 cachedValue를 리턴한다!!
- 결국 이런 상황이 되면 std::atomic보단 std::mutex를 사용하는 게 바람직하다.
결론
- 스레드에 자유로운 상황이 점점 드물어지고 있으므로 const 멤버 함수는 늘 스레드 안전성을 고려하자.
- 그리고 그런 구현을 할 때는,
- 동기화가 필요한 변수가 하나일 때는 std::atomic
- 둘 이상일 때는 std::mutex를 사용하자.
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 18. std::unique_ptr (0) | 2025.02.28 |
---|---|
[Effective Modern C++] 17. 특수 멤버 함수(special member function) (1) | 2025.02.27 |
[Effective Modern C++] 15. constexpr (0) | 2025.02.25 |
[Effective Modern C++] 14. noexcept (0) | 2025.02.24 |
[Effective Modern C++] 13. const_iterator 선호하기 (0) | 2025.02.21 |
Comments