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의 초기화 문법을 써봐야 데드락이 걸리긴하지만, (원래 의도일) 스코프 락을 제대로 획득한 상황은 아니다.
난 scoped lock 을 주로 __COUNTER__ 를 매크로랑 같이 써서 unique 한 이름으로 만들어서 사용하는데 너무 옛날 스타일인건가;
헐 그렇게 쓰면 좋겠네요. 이름짓는 것도 일이라
락이 블록 스코프동안 유효하려면, 애초에 boost::mutex::scoped_lock lck(mutex_);처럼 했어야 하지 않나요?
네. 말씀하신대로 입니다. unnamed 객체의 수명은 해당 expression 동안입니다. 예제가 적당하질 않군요.