Lock, lock, lock … #2
Rica가 내 이전 글에 링크로 올려 준 beautiful code의 일부분에 해당하는 글을 지금 막 (제대로) 읽었음. 기억해 둬야할 점 + 읽다가 떠오른 것들을 정리해본다.
기억해야 할 것들 – lock이 좋지 않은 이유
- (필요한 것 보다) 적은 수의 lock을 얻는 것은 문제가 된다
- (필요한 것 보다) 많은 수의 lock을 얻는 것은 문제가 된다 – 심하면 데드락, 안 심해도 성능에 피해를 준다.
- 엉뚱한 lock을 잡고 있을 수 있다 – lock과 데이터 사이엔 명확히 정의되는 상관관계가 있는 것이 아니다.
- 잘못된 순서로 lock을 잡으면 문제가 된다 – 데드락이 발생할 수 있다.
- 에러 리커버리 문제 – lock을 잡고있는 상태에서 에러 혹은 예외가 전달 될 때, 이미 가지고 있던 lock들을 제대로, 그리고 정확한 순서로 내려놔야 한다.
(사실 이것 말고 lock의 가장 기본적인 문제 – 모듈러 프로그래밍과 잘 안 어울리는 것, 정확한 conditional variable 사용문제 등등도 언급되어 있었음)
처음 3가지는 적합한 해결책은 없다고 생각한다. 적은 수의 lock을 얻는 문제는 적절한 axiomatic 프로그래밍으로 해결할 수 – 예를 들어 공유 데이터 변경 진입부에서 각 lock을 가지고 있는지 확인 – 도 있지만, 완결된 – 그러니까 거의 프로그래머의 간섭없이는 해결되지 않는- 문제가 아니다. 너무 많은 lock을 잡는 문제는 자주 보게되는데, MT 프로그래밍 자체가 굉장히 보수적으로/비관적으로 진행되는 경향이 있기 때문에 어쩔 수 없는건지도 모르겠다 – 사실 여기서 성능을 왕창 까먹는거다.
4, 5 번째 문제의 경우 부분적인 해결책은 존재한다고 생각한다(특히 C++및 유사 언어들의 경우).
우선 프로그래머의 신경을 어느 정도 잡아먹겠지만, per-thread로 자기가 잡고 있는 lock들의 set을 정의 하고, 이에 기반해서 확인하는 방법을 만들 수 있다. 예를 들어 A = { a1, a2, …, an }
, B = { b1, b2, … , bm }
, C = { c1, c2, …, ck }
라는 공유 데이터 집합들에 대해서 lock을 잡는 경우를 생각해보자. A, B, C 세 집합에 대해서 응용프로그램에서 사용할 형태의 directed acyclic graph (DAG) 를 만들고이 순서대로만, 그리고각 집한 안에서는 첨자; subscript가 증가하는 순서대로만 lock을 잡게 하는 거다. 그러면 integer set 3개를 가지고 제대로 lock을 잡는지 확인할 수 있다. (A -> B -> C 순서로만 허용된다면, 테스트 해야하는 것은 일종의 순서다. b3의 lock을 잡는다면, b4 이후를 잡았는지, C의 원소 중에 잡은 lock이 있는지 확인하면 된다)
그리고 RAII를 사용한 lock 잡기의 경우, 5번째 항목의 조건을 거의 완벽하게 만족한다. C++에서 WrappedLock이란 것을 만들고(이름에 신경쓰진 말자), “lock을 잡는다 = 생성자에 인자로 넘겨준다“로 하고, “소멸자의 호출 = lock을 놓는다“로 생각하고 사용하면 안전하게 lock을 잡고, 놓으면서도, 언제나 정확한 순서 / 에러 전달 안정성이 보장된다.
(사실 생성자/소멸자를 이용한 자원의 획득/해제 – 그러니까 RAII – 가 이런 의미를 제공해주는 것이다)
ps. 사실 RAII를 사용하는 방법은 C++ 유사 언어 중 일부분 – 정확히 말하면 소멸자 호출 시점이 scope 룰로 보장되지 않는 좀 더 modern하다는 언어들(C#, Java, …) 에서는 불가능하다.