Double-checked locking 이디엄의 함정

이하의 정보는"C++ Concurrency in Action"에서 얻은 것임을 밝힙니다.
(참고로 아직 정식 출간되지 않은 이 책의 맛보기 버전을 여기에 있는 쿠폰코드를 사용하면 싸게 구입하실 수 있습니다.)


Double-checked locking 패턴이라는 놈이 있습니다. 저는 "Head First Design Pattern" 책에서 처음 접했습니다. 싱글톤 등에서 lazy initialization 이 필요할 때, 유용하게 쓰인다고 추천되어 있었습니다. 물론 병렬 환경에서의 이야기입니다. 한마디로 C++로 표현하면 다음과 같은 경우지요.


다중스레드 환경에서 이를 안전하게 돌리려면, 간단히 뮤텍스 등으로 검사 및 초기화 부분을 묶어주면 되지만, 이 경우 한번의 초기화를 위한 비용이 너무 크게 됩니다. 한번 초기화되고 나면 필요없는 부분에 대해 계속 락을 걸어줘야 하는 것이죠. 이를 해결하기 위해 나온 것이 Double-checked locking입니다.


하지만 여기에는 중대한 함정이 숨어있습니다. 바로 data race 입니다. 3번째 줄의 if문 검사는 전혀 동기화되지 않는다는 것이죠. 여기서 동기화란 메모리 읽기/쓰기의 정렬을 말합니다. 반면 뮤텍스가 보호하는 5~9번째 줄은 적절히 동기화가 이루어집니다. 하지만 그와 3번째 줄 수행과의 동기화는 이루이지지 않습니다. 가령, 스레드 A에서 5~9번째 줄을 수행했다고 해보죠. 그래서 some_resource를 할당하고, 생성자를 호출한 후, resource_ptr를 적절히 설정했습니다. 하지만 이러한 작업의 결과가 스레드 B에서는 다른 순서로 보일 수 있습니다. 컴파일러나 하드웨어의 재정렬이라던가 메모리 계층구조 등에 의해 스레드 B에는 그 결과는 다른 순서로 전달될 수 있는 것이죠. 그래서 스레드 B가 3번째 줄을 수행하면서 검사를 할 때, resource_ptr는 설정이 되었지만, 실제 some_resource는 생성자로 적절히 초기화된 상황이 아직 반영이 안되어 있을 수 있습니다. 이 경우 당연히 do_something() 호출에서 문제가 생기겠죠. 이것이 data race입니다.

이렇게 메모리가 상태가 스레드 간에 다른 순서로 보이는 문제를 막기 위해 메모리 배리어란 것이 존재합니다. 간단히 말해 그 지점까지 메모리 동기화를 완료하도록 울타리를 치는 개념입니다. 보통 뮤텍스 같은 동기화 개체를 사용하면 자동으로 메모리 배리어가 삽입된다고 합니다. 하지만 3번째 줄의 검사는 무방비이므로 resource_ptr 설정 부분이 동기화로 묶여 있더라도 실제 3번째 줄을 수행하는 스레드가 어떻게 메모리를 보게 될지는 장담할 수없는 것이죠. 이러한 double-checked locking의 함정에 대해서는 여기여기에서 자세한 설명을 볼 수 있습니다.

다행히 C++0x에서는 이러한 병렬 환경에서의 lazy initialization 을 위해 std::call_oncestd::once_flag 라는 전용 메카니즘을 제공합니다. 간단히 다음과 같이 사용합니다.


여러모로 기대되는 C++0x 입니다. 그만큼 새로 공부해야할게 많지만요.
여하간 정말 조심하지 않으면 함정에 빠지기 쉬운 병렬 세계입니다.
크리에이티브 커먼즈 라이선스
Creative Commons License
Trackback 0 Comment 2

top