간만에 책이 아닌, 내 생각의 정리를 포스팅을 한다. 이 포스트가 "데드락을 피하는 방법" 이라든지 "락을 거는 시기를 아는 방법" 이라든지 "락의 종류에 따른 효율성" 등의 이야기를 다루고 있지는 않는다.

이 생각의 정리는 "멀티 쓰레드 기반에서 C++로 어떻게 편하게 락을 걸까?" 이다. 그렇다면, 기존의 락을 걸던 방법들과 불편한점들에 대해서 정리해 보자.

초기의 락을 거는 방법은 다음과 같다.

나는 처음 "임계영역"이라는 개념을 알았을 때, 이렇게 사용 하고 있었다. 하지만 사용 중, 위에서 초록색 색을 칠한 부분에서 "return"을 하게 되면, 두번째 f() 호출시 무조건 "데드락"이 걸리게 된다. 왜냐하면 Step4에서 "임계영역을 빠저나갔어!" 라고 알려주지 않았기 때문이다.

여기서 return에 한정되게 말했지만, "return"이 될 수 있는 "예외발생" 역시도 마찬가지이다. 그러므로 나의 생각은 자연스럽게 "return이 될 때, 자동으로 호출해 주는 방법이 없을까?" 란 생각을 하게 되었다.

어느날 책을 보다가 그 대안이라고 나왔던 기술이 헬퍼 클래스를 이용하여, 객체 소멸시 락을 푸는 것인데 "객체가 소멸 될 때, 소멸자에게 뒷처리를 맡기기"였다. 이것을 코드로 옮기면 다음과 같다.

이렇게 됨으로써, 초록색으로 칠한 연산하는 영역에서 return 이 되게 된다고 하더라도, 알아서 임계영역을 걸었던 것이 풀리게 될 것이다.

하지만 이 코드는 쓰면 쓸수록 나에겐 몹시 이상하게 보였다. 그 이유는 "다큐 프라임, 동과 서 2부작"을 보고 부터였다.

영어 공부 때문에 이 동상을 보게 되었는데, 보는 순간 C/C++ 도 서양에서 만들어진 언어이므로, 서양 사람들이 어떻게 생각하는지 안다면, 보다 더 C/C++을 잘 사용할 수 있지 않을까? 란 생각이 많이 들었다.






그러므로 각 코드를 분리하여 독립적으로 생각하게 되었고, 기존의 방법이 독립적이지 못하며, 다음의 문제점들이 있다는 것을 발견하였다.

, 함수 자체가 락을 걸고 있기 때문이다. 이것은 락이 "스핀 락"등으로 변경이 되었다면, 함수 자체도 변경 되는 것을 감수해야 한다는 것을 의미한다. "뭐 다시 컴파일 하면 되지"라고 생각한다면 가볍거 넘아갈 수 있다.

둘째, 락이 걸리지 않은 함수를 만들지 못한다. 락이 필요가 없는 함수를 써도 될것 같다고 생각하고, 락을 없애려고 한다면, 해당 함수를 찾고 락을 건 부분을 주석처리 하게 될 것이다. 이때도 마찬가지로 "뭐 다시 컴파일 하면 되지" 라고 생각해서 넘어가도 될 것이다

셋째, 변수 단위, 객체 단위, 함수 단위 등의 임계영역 설정이 번거럽다. 함수 단위야 기존의 방법대로 함수 자체의 제일 첫 부분에 락을 걸면 그만 이지만, 변수 단위와 객체 단위에 대해선 스코프{ }를 걸어 들여쓰기를 신나게 해야 한다. "뭐 걸면 되잖아?" 라고 생각한다면 그냥 넘어가도 될 것이다.

넷째, 사용하는 라이브러리가 멀티 쓰레드에 안전하지 못하다면, 락 때문에 Wrapper 함수/클래스 만들들어서 사용 할 수밖에 없다. 대표적으로 std::vector 컨테이너들을 사용 할 때 이다.

이 4가지 문제들은 "임계영역" 자체를 함수의 역활 속에 포함하는 방법 때문인데, 이것은 상속 다음으로 의존 관계가 강한 상태이다. (이것이 상속이 나쁘다, 포함관계가 나쁘다를 의미하지는 않는다.)

그러므로 나는 이 문제를 해결하려고 골똘히 생각하고 있다가, "데브루키의 닭이좋은기원(최기원)씨의 Loki의 스마트 포인터 이야기"를 듣고 실마리가 다 풀렸다.

그 실마리는 "임시 객체로 락을 걸 수 있다." 였다. 지금에서야 이 말은 "임시 객체의 생명주기(생성과 소멸)를 이용하여, 임계영역을 만들 수 있다."로 풀이 할 수 있다.  스터디 후에 집에 돌아와 코드를 끄적이다가, 가능성을 테스트 해보았고, 만족할만 하다고 생각하여 여기서 공개하여 토론을 하고자 한다.


임시 객체의 생명주기(생성과 소멸)을 이용하여, 임계영역을 걸었을 때의 코드

- 위 코드에서 개선해야 하는 점

  1. AutoLock이 boost::bind( AutoLock, ...  ) 이나 AutoLock<Locker>( &lock, boost::bind() ) 를 지원하면 사용시 편할 것이다.
  2. AutoLock이 함수에 대해서 특화시켜 AutoLock<Locker>( &lock, func )( a, b ... )를 지원하면 사용시 편할것 같다.
  3. AutoLock 에서 Target에 해당하는 두번째 인자가 const 일 경우도 추가해 주면 더 편해짐
  4. AutoLock 의 타겟이 값일 때는 operator* 의 인터페이스만 제공하고, 포인터일 때는 operator->만 제공해야 한다.
  5. AutoLock의 타켓이 임시객체일 경우, 값 복사를 할 수 있게 개선시켜야 한다.


테스트 예제

posted by 농사를 짓는 게임 프로그래머 최익필

댓글을 달아 주세요

  1. Favicon of http://blog.naver.com/rkawk01 감자 2009.12.02 09:52  Addr  Edit/Del  Reply

    오와~ 좋네요
    전 항상 첫번째 방법만 애용했었는데..
    담아갑니다~ http://blog.naver.com/rkawk01/memo/70074819409