Post List

2015년 4월 4일 토요일

item 16: const 멤버함수라고 해서 thread 안정성이 보장되는건 아니다.

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








Effective Modern C++ - Scott Meyers
Item 16: Make const member functions thread safe.

item 16. const 멤버 함수라고 해서 thread 안정성이 보장되는건 아니다.

Scott 아저씨가 갑자기 뜬구름없이 아이템16에서 왜이러실까요 ?
"저... 여기서 이러시면 안됩니다." 라고 강려크하게 말하고 싶지만...
이런건 저~ 뒤에 챕터 7 Concurency API 부분에서 다뤄되 될껀데... 라고 생각이 들지만,
일단 언급된 내용이니 살펴 보도록 하겠습니다.

엄청나게 복잡한 계산을 한 결과 값을 가지는 클래스가 있다고 가정해 보겠습니다.
그 값은 계산하는데 많은 연산을 필요로 하고, 한번 정해진 값은 바뀔 필요가 없는 값이라고 할 경우
효율성을 위해서 해당 값을 반환하는 함수를 const로 정의하고 싶습니다.

class Widget
{
private:
    mutable bool IsValidValue { false };
    mutable double AbsoluteValue;

public:
    double GetAbsoluteValue() const
    {
       if (!IsValidValue)
       {
           // ... 겁나 복잡한 계산식후에 AbsoluteValue 값이 정해짐
           IsValidValue = true;
       }
       return AbsoluteValue;
    }
};

mutable 키워드는 const 함수 안에서도 수정을 가능하게 할 경우 사용됩니다.
(솔직히 이럴려면 const를 안써야 하지 않냐는게 제 생각이긴 합니다만...)
위 Code는 C++98, C++11 에서 모두 허용됩니다.

const로 선언된 함수의 경우 여러 쓰레드에서 동시에 접근하더라도 별도의 안전장치 없이 동시에 수행됩니다.
하지만 이 경우는 값을 수정하는 Code가 있으므로, 그랬다간 문제가 있을 수 있다는 생각이 들겠죠 ?
어차피 중복해서 실행될 뿐 문제는 없을꺼라고 생각 할 수도 있겠지만,
겁나 복잡한 계산식에서 계산과정의 중간값을 무조건 다른 변수에 저장하고,
하나의 변수에 값을 2번이상 대입하는 경우가 전혀 없는게 아니라면, 문제가 발생할 수 있습니다.

그러니, 멀티쓰레드 환경에서 안정적으로 동작할 수 있게 std::mutex를 적용하도록 하겠습니다.
RAII로 동작하도록 std::lock_guard를 사용하도록 하죠.

class Widget
{
private:
    mutable std::mutex M;
    mutable bool IsValidValue { false };
    mutable double AbsoluteValue;

public:
    double GetAbsoluteValue() const
    {
       std::lock_guard<std::mutex> LOCK(M);

       if (!IsValidValue)
       {
           // ... 겁나 복잡한 계산식후에 AbsoluteValue 값이 정해짐
           IsValidValue = true;
       }
       return AbsoluteValue;
   }
};

std::mutex역시 const함수 안에서 값이 바뀌어야 하므로 mutable 로 선언하였습니다.
그런데... 아 ! 그런데...
Concurrency를 좀 보신 분들은 Lock-free 라는 말을 들어보셨을 껍니다.
Lock을 거는 행위 자체가 얼마나 성능에 큰 영향을 미치는 지에 대해서는 귀에 딱지가 생기도록 들었을 껍니다.
std::mutex는 좀 과한것 같고, Lock-free로 동작이 가능한 std::atomic을 사용한 Code로 수정해 보겠습니다.

class Widget
{
private:
    mutable std::atomic<bool> IsValidValue { false };
    mutable std::atomic<double> AbsoluteValue;

public:
    double GetAbsoluteValue() const
    {
        if (!IsValidValue.load())
        {
            // ... 겁나 복잡한 계산식후에 AbsoluteValue 값이 정해짐
            IsValidValue.store(true);
        }
        return AbsoluteValue.load();
    }
};

이제 좀 light하게 구현되었습니다.
참고로 std::mutex와 std::atomicCOPY가 불가능하고 MOVE만 가능한 개체이므로,
Widget 또한 COPY가 불가능하게 됩니다.
음.. .그런데 과연 저 Code가 문제 없는 Code일까요 ?
그럴 수도 있고 아닐 수도 있습니다.
그 문제는 바로 아래에서 다뤄보도록 하고 일단 넘어가겠습니다.

오호라..  std::mutex보다는 std::atomic이 더 좋다고 ?
그럼 이제 본격적으로 std::atomic을 써볼까 ? 라고 생각 할수도 있습니다.
(아놔~~ 이런 얘기는 Concurrecny API에서 할 이야기인데...)
하지만 std::atomic은 하나의 값에 대해서 적용할때는 괜찮은데, 2개 이상의 값에 대해서 제어할 경우 원하는대로 않을 수 있습니다.
그럴땐 std::mutex를 사용해야 합니다.

왜 그런지 한번 다음 Code를 보면서 생각해 보죠.

class Widget
{
private:
    mutable std::atomic<bool> IsValidValue { false };
    mutable std::atomic<double> AbsoluteValue;

public:
    double GetAbsoluteValue() const
    {
       if (!IsValidValue.load())
       {
           auto V1 = VeryExpensiveComputation1();
           auto V2 = VeryExpensiveComputation2();
           AbsoluteValue.store(V1 + V2); // Part 1
           IsValidValue.store(true);     // Part 2
       }
       return AbsoluteValue.load();
    }
};

첫번째 쓰레드가 GetAbsoluteValue()를 호출 했을 경우, IsValidValue가 false라 if구문 안으로 들어간 경우,
Part 2가 수행되기 전에 두번째 쓰레드가 실행된 경우 역시 if구문 안으로 들어갈 수 있습니다.
그럼 Part 1과 Part 2를 바꾸면 해결 될까요 ?

class Widget
{
private:
    mutable std::atomic<bool> IsValidValue { false };
    mutable std::atomic<double> AbsoluteValue;

public:
    double GetAbsoluteValue() const
    {
       if (!IsValidValue.load())
       {
           auto V1 = VeryExpensiveComputation1();
           auto V2 = VeryExpensiveComputation2();
           IsValidValue.store(true);     // Part 2
           AbsoluteValue.store(V1 + V2); // Part 1
       }
       return AbsoluteValue.load();
    }
};

이때 첫번째 쓰레드가 Part 2까지 수행을 하고 아직 Part 1 수행전에,
두번째 쓰레드가 GetAbsoluteValue()를 호출하면 AbsoluteValue에 값이 저장되기 전이라서 의미없는 값을 전달하게 됩니다.

그럼 결국...
std::mutex대신 std::atomic을 적용하고자 했던 야심찬 계획은 수포로 돌아갔습니다.
std::mutex를 사용할 수 밖에 없습니다.

class Widget
{
private:
    mutable std::mutex M;
    mutable bool IsValidValue { false };
    mutable double AbsoluteValue;

public:
    double GetAbsoluteValue() const
    {
       std::lock_guard<std::mutex> LOCK(M);

       if (!IsValidValue)
       {
           auto V1 = VeryExpensiveComputation1();
           auto V2 = VeryExpensiveComputation2();
           IsValidValue = true;   // Part 2
           AbsoluteValue = V1 + V2;      // Part 1
       }
       return AbsoluteValue;
    }
};

Things to Remember

const 멤버 함수는 절대로 멀티 쓰레드에서 제어 할 필요가 없다고 확신되는 경우를 제외하고는 쓰레드 안정성을 생각해서 구현해야 합니다.

std::atomic은 std::mutex보다 더 좋은 성능을 보장해주지만, 1개의 값에 대해서만 적합한 방법입니다.

댓글 없음:

댓글 쓰기