[Effective Modern C++] 16. const멤버 함수를 스레드에 안전하게 작성하자
멤버 함수가 멤버 변수들을 수정하지 않는다면 const로 선언하는 것이 자연스럽다. 그런데 스레드를 사용하면 문제가 생길 수 있다.
아래의 예시를 봐보자.
mutex 사용
다음은 다항식의 근을 구하는 함수 roots를 구현하는 내용이다. 성능 향상을 위해 캐싱을 이용하였으며, rootsAreValid의 값을 이용하여 캐싱 여부를 판별한다.
class Polynomial
{
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
if(!rootsAreValid)
{
...
rootsAreValid = true;
}
}
private:
mutable bool rootsAreValid { false };
mutable RootsType rootVals {};
};
그런데 여기서 두 개의 스레드가 roots 함수를 호출하는 경우를 생각해보면, rootsAreValid와 rootVals는 스레드에 안전하지 않기 때문에 mutex를 써야 한다. 문제는 roots 함수가 const함수이기 때문에 mutex 또한 mutable로 선언해야 한다.
mutable : 멤버 변수의 값을 const함수에서도 바꿀 수 있게 해주는 키워드
class Polynomial
{
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
std::lock_guard<std::mutex> g(m);
if(!rootsAreValid)
{
...
rootsAreValid = true;
}
}
private:
mutable std::mutex m;
mutable bool rootsAreValid { false };
mutable RootsType rootVals {};
};
하지만 mutex는 복사하거나 이동할 수 없기 때문에 mutex를 클래스에 추가하면 해당 클래스의 복사와 이동도 불가능해진다.
atomic 사용
멤버 함수의 호출 횟수를 세고 싶다면, atomic 카운터(항목 40 참고)를 사용해서 비용을 줄일 수 있는 경우가 많다. (실제로 비용이 절감되는지는 프로그램이 실행되는 컴퓨터 하드웨어에 혹은 표준 라이브러리의 구체적인 뮤텍스 구현 방식에 따라 다를 수 있다.)
다음은 atomic을 이용하여 멤버 함수의 호출 횟수를 세는 방법을 보여주는 예제 코드이다.
class Point
{
public:
double distanceFromOrigin(void) const noexcept
{
++callCount;
return std::hypot(x, y); // std::sqrt(x*x + y*y)
}
private:
mutable std::atomic<unsigned> callCount { 0 };
double x, y;
};
mutex처럼 atomic도 복사와 이동이 불가능하다. 따라서 Point에 이 callCount를 도입하면 Point 역시 복사와 이동이 불가능해진다.
여기서 mutex보다 비용이 싸다고 atomic을 남발하면 문제가 생길 수 있는데 아래의 예제를 봐보자.
class Widget
{
public:
int magicValue(void) const
{
if (cacheValid) return cacheValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValue = val1 + val2;
cacheValid = true;
return cacheValue;
}
}
private:
mutable std::atomic<bool> cacheValid { false };
mutable std::atomic<int> cacheValue;
};
이 코드가 작동하긴 하지만 생각보다 비용이 클 수 있다.
1. 한 스레드가 Widget::magicValue를 호출한다. cacheValid가 false라고 관측하고 비용이 큰 두 계산을 수행한 후 둘의 합을 cacheValue에 대입한다.
2. 그 시점에서 둘째 스레드가 Widget::magicValue를 호출하는데 역시 cacheValid가 false라고 관측해서 첫 스레드가 방금 마친 것과 동일한 비싼 계산들을 수행한다.(여러 개의 다른 스레드일 수도 있음.)
cacheValue와 cacheValid의 대입 순서를 바꿔도 위와 동일한 문제가 발생할 수 있으며, 추가적으로 아래와 같은 문제도 발생할 수 있다.
1. 한 스레드가 Widget::magicValue를 호출해서 cacheValid가 true로 설정되는 지점까지 나아간다.
2. 그 시점에 둘째 스레드가 Widget::magicValue를 호출해서 cacheValid를 점검한다. 그것이 true임을 관측한 둘째 스레드는 첫 스레드가 cacheValue에 값을 대입하기 전에 cacheValue를 돌려주므로 그 반환 값은 정확하지 않다.
즉, 동기화가 필요한 변수 하나 또는 메모리 장소 하나에 대해서는 atomic을 사용하는 것이 적합하지만 둘 이상의 변수나 메모리 장소를 하나의 단위로서 조작해야 할 때는 mutex를 꺼내는 것이 바람직하다.
Widget::magicValue를 뮤텍스로 보호한다면 다음과 같은 모습이 될 것이다.
class Widget
{
public:
int magicValue(void) const
{
std::lock_guard<std::mutex> guard(m); // m을 잠근다.
if (cacheValid) return cacheValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValid = true;
cacheValue = val1 + val2;
return cacheValue;
}
} // m을 푼다.
private:
mutable std::mutex m;
mutable int cacheValue; // 이제는 atomic이 아님
mutable bool cacheValid { false }; // 이제는 atomic이 아님
};
요약
- 동시 컨텍스트에서 쓰이지 않을 것이라고 확신하지 않는 다면, const 멤버 함수는 스레드에 안전하게 작성해라.
- atomic 변수는 mutex에 비해 성능상의 이점이 있지만, 하나의 변수 또는 메모리 장소를 다룰 때만 적합하다.
'Books > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라 (0) | 2022.09.18 |
---|---|
[Effective Modern C++] 17. 특수 멤버 함수들의 자동 작성 조건을 숙지하자 (0) | 2022.09.11 |
[Effective Modern C++] 15. 가능하면 항상 constexpr을 사용하자 (0) | 2022.09.11 |
[Effective Modern C++] 14. 예외를 방출하지 않을 함수는 noexcept로 선언하자 (0) | 2022.09.04 |
[Effective Modern C++] 13. iterator보다 const_iterator를 선호하자 (0) | 2022.09.04 |
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라
[Effective Modern C++] 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라
2022.09.18 -
[Effective Modern C++] 17. 특수 멤버 함수들의 자동 작성 조건을 숙지하자
[Effective Modern C++] 17. 특수 멤버 함수들의 자동 작성 조건을 숙지하자
2022.09.11 -
[Effective Modern C++] 15. 가능하면 항상 constexpr을 사용하자
[Effective Modern C++] 15. 가능하면 항상 constexpr을 사용하자
2022.09.11 -
[Effective Modern C++] 14. 예외를 방출하지 않을 함수는 noexcept로 선언하자
[Effective Modern C++] 14. 예외를 방출하지 않을 함수는 noexcept로 선언하자
2022.09.04