C++11: scoped_lock + “짜증나는 파싱”

boost::thread 에는 scoped_lock 이라는 블럭 스코프 동안 유효한 락이 있다. 멀티스레드 프로그래밍 하는 사람들은 유용하게 쓰고 있으리라 생각한다.
이 클래스를 엉터리로 쓰고 있는 다음 코드를 보자. main 에서 미리 mutex 에 락을 걸고 들어가기 때문에, scoped_lock 이 락을 걸지 못해서 데드락에 빠질 것처럼 보인다.

#include <iostream>

#include <boost/thread/mutex.hpp>


struct foo {
  boost::mutex mutex_;

  void Run() {
    boost::mutex::scoped_lock(mutex_);
    std::cout << "Should not be reached" << std::endl;
  }
};


int main (int argc, char** argv) {
  foo bar;
  bar.mutex_.lock();  // 미리 mutex 를 획득; 데드락을 의도함
  bar.Run();
  bar.mutex_.unlock();
  return 0;
}

하지만 그렇지 않다.

“Should not be reached” 를 출력하고 프로그램은 정상 종료한다. 왜 그런가 설명하자면, C++의 가장 짜증나는 파싱 규칙 (C++’s (most) vexing parse) 때문.

C++의 가장 짜증나는 파싱 (C++ ‘s most vexing parse) 은 아주 거칠게 말하자면, 선언으로 해석할 수 있는건 선언으로 해석한다 란 의미다. 그래서 boost::mutex::scoped_lock(mutex_)mutex_ 란 이름의 scoped_lock선언 한다. mutex_ 에 대한 scoped_lock 을 초기화하는게 아니라.
문법적으론 둘 다 가능해 보이지만, 실제로는 선언으로 우선 해석하기에 foo::Run() 은 락을 획득하지 않는다.

대충 이런 느낌의 코드가 지난 주에 회사 코드베이스에서 나와서 “나는 리뷰 때 무얼했는가”라면서 괴로워함 ㅠㅠ

C++11 이후에는 이 문제가 아주 간단히 풀린다. “Effective Modern C++”의 Item 7에서 나오는 것처럼, 괄호가 아니라 중괄호를 쓰면 이런 문제가 없다. 변수 선언인지 초기화인지를 의도에 따라 적당한 문법을 쓸 수 있다. 다음과 같이 바꾸면 의도한 대로 데드락에 빠진다.

struct foo {
  boost::mutex mutex_;

  void Run() {
    boost::mutex::scoped_lock {mutex_};  // 초기화 목록으로 변경; 하지만 정상적인 동작은 아니다
    std::cout << "Should not be reached" << std::endl;
  }
};

그런 의미에서 C++11 이후를 씁시다! (…)

Updated : 아래 dkim 님이 코멘트한대로 이건 의도한 동작이 되는 것은 아니다.
C++에서 이름 없는 변수의 생명 주기는 expression 이 끝나는 순간 소멸한다. 그래서 C++11의 초기화 문법을 써봐야 데드락이 걸리긴하지만, (원래 의도일) 스코프 락을 제대로 획득한 상황은 아니다.

Jinuk Kim
Jinuk Kim

SW Engineer / gamer / bookworm / atheist / feminist

Articles: 935

4 Comments

  1. 난 scoped lock 을 주로 __COUNTER__ 를 매크로랑 같이 써서 unique 한 이름으로 만들어서 사용하는데 너무 옛날 스타일인건가;

  2. 락이 블록 스코프동안 유효하려면, 애초에 boost::mutex::scoped_lock lck(mutex_);처럼 했어야 하지 않나요?

    • 네. 말씀하신대로 입니다. unnamed 객체의 수명은 해당 expression 동안입니다. 예제가 적당하질 않군요.

Leave a Reply