Lock, lock, lock …

멀티 스레드 프로그램을 작성하는 경우 – 웹 서버처럼 스레드 상호간의 연관성이 적은 경우를 빼면 – 스레드 간에 공유 자원에 접근하는 부분은 언제나 문제가 된다.

제대로 작성하는 방법은 크게 3 2가지다.

  • 대충 짜고 행운을 기대한다 (Luck-oriented programming) – 주변의 사례들에서 이런 경우가 상당히 많다(당연히 안돌고 디버깅한다 낄낄)
  • Lock을 써서 해당 영역을 하나의 스레드만 접근하게 한다 – 적절한 난이도
  • Lock-free 한 구성을 만들어 낸다 – 잘 정의된 몇 가지 경우를 제외하고는 적용하기가 매우 힘들다(…)

우선 그나마 적절한 난이도를 갖는 lock을 사용한 프로그래밍에 관해서 이번 포스팅에서 몇 가지를 설명하려고 한다.
Lock은 일종의 문처럼 동작하는데, 특정 스레드가 해당 문에 들어가면서 + 동시에 문을 잠글 수 있는 기능을 한다 (사실 이게 비유로는 조금 부적절한게, 문을 통하지 않고도 들어갈 수 있기 때문에 -_-; 모두가 규칙을 지켜줘야한다.) 다른 스레드가 이미 잠긴 문을 들어가려하면, 잠겨 있기 때문에 들어갈 수 없고, 이전 스레드가 문을 열고 다시 나오기를 기다리게 된다.

Lock이 사용되는 형태는 매우 단순하다. 동시에 2개의 스레드에서 실행해선 안되는 코드 – 예를 들어 웹 서버에서 각 IP별 접근 횟수를 기록하고 있고, 이게 하나의 함수 안에서 이루어진다고 하자 – 가 있을 때, 해당 코드의 시작/끝을 Lock으로 감싸는 형태를 띈다. 이 부분을 RAII 형태로 사용하거나, 좀 더 예쁜 구문으로 사용하는 방법은 리카넷: synchronized block in C++을 참고하자.
예를 든 걸 코드로 간단히 표현해보면,

void increment_ip_counter( const int32_t ipv4_address )
{
// blah blah
lock.acquire();
counter[ ipv4_address ] += 1;
lock.release();
}

정도가 될 것이다. (counter는 std::map< int32_t, unsigned int64_t > 정도로 생각해 주자)
동시에 2개 이상의 스레드가 카운터를 바꾸는 것만 막으면 문제가 안되기 때문에 이런 형태로 보호하면 된다. 상당 수의 lock을 사용하는 코드는 이런 형태로 되어 있다.

사실 문제가 되는 것은 이런 1개의 lock을 사용한 경우가 아니다. (물론 priority inversion, lock convoy 같은 문제가 존재하긴한다)
복수의 lock을 획득하는 경우가 주로 문제가 된다. 구체적으로 말하면 dead-lock이 발생할 수 있다.
(우선 보통 쓰이는 어휘를 간단히 설명하면 lock을 잡는다/획득한다/얻는다는 lock을 잠그는 일 (lock; acquire)이고, lock을 놓는다/내려놓는다/해제한다는 것은 lock을 푸는 일 (unlock; release)에 해당한다.)

Dead lock이란 것은 2 개 이상의 스레드가 서로가 다른 스레드가 이미 가지고 있는 lock을 원해서 무한히 대기하게 되는 상황을 말한다. 예를 들어 스레드 T1이 lock L1, L2를 가지고 있고, 스레드 T2가 lock L3를 가지고 있는데, T1이 L3를, T2가 L2를 추가로 가지기 원하는 상황이 되면 T1은 T2가 lock을 내려놓기를, T2는 그 반대를 기다리게 된다.

이걸 어떻게 피해갈까? 많은 현대 OS나 MT 프로그래밍에서 많이 사용되는 방법은 굉장히 단순하다. 모든 lock에 번호를 붙이고 (실제로 lock을 획득하는 경로에 대해 strict ordering이 나오기만 하면 된다), 여러 개의 lock을 잡을 때에는 항상 이 순서를 유지하는 것이다. 간단한 방법이지만 이 방법 하나로 모든 dead lock을 회피하게 된다.

직전에 언급한 예제의 경우, T2가 L3를 이미 잡고 있기 때문에, 그보다 번호가 작은 L1이나 L2를 요청하는 경우는 lock ordering을 사용하는 경우 발생할 수가 없고, 이에 따라 여러 스레드가 서로가 서로를 요구하는 (lock을 요구하는) cycle이 생기질 않는다.

Multi thread의 세계에서 일하는 것은 힘들다. 그렇지만 몇 가지 원칙 – 여기에서는 lock을 잡는 순서만 언급했지만 – 을 지키면 그나마 다룰만한 무언가가 된다. 그런 의미에서 lock을 어떻게 잡아야 dead lock을 피할 수 있는지 꼭 기억하자[…]

ps. 이 글 역시 지금 참여 중인 프로젝트 코드에 저런게 있었기 때문에 나온거라곤 말 못한다.

Published by

rein

나는 ...

10 thoughts on “Lock, lock, lock …”

  1. 나도 락킹의 프로그래밍 모델을 깔끔하게 세워보려고 이리저리 생각중인데 쉽지가 않다.

    요즘 Beautiful code 책의 한 챕터로 추정되는 Beautiful concurrency를 읽는 중.
    ( http://research.microsoft.com/~simonpj/papers/stm/beautiful.pdf )

    앞쪽 약간 읽었는데, 락킹의 어려움에 대해 친절하게 (=눈물나게) 설명하고 있음. STM=소프트웨어 트랜잭셔널 메모리를 대안으로 제시하고 있는데, 어떤 건지 기대가 된다.

  2. 그 뭐랄까 요즘 MT 쪽의 새로운 빌딩 블럭을 제안하는 게 많은 것 같은데, 아무래도 지금 스레드 쪽 뭔가를 만들어내기 위한 기초 데이터? 루틴? 같은게 부족한거 같음.
    Lock, thread, conditional variable, mutex 이런건 너무 원시적; primitive 이지;

    모 블로그에 트랙백 날리는 중에 모 블로그 주인장 답글이 달렸다[…]

  3. 다익스트라 영감님 같은 분이 나와서 Lock Considered Harmful 같은 거라도 써 주시게 될까 낄낄
    검색해 보니까 Caps-Lock Considered Harmful은 많이 나오네…
    Threads Considered Harmful은 있군.

  4. 낄낄

    근데 뭐랄까 considered-harmful이라도, 더 이상 CPU 속도가 빨라지기 힘드니 스레드가 사실 상 존재하는 성능 향상의 마지막 대안이라는 생각들도 요즘 꽤 많지
    좀 된거지만 http://gotw.ca/publications/concurrency-ddj.htm : The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software by Hurb Sutter 이런 글도

  5. 병렬 인트린식들을 써서, 처리 흐름은 하난데 개별 데이터 처리를 병렬화.. 하는 식의 완전 다른 접근도 있는 듯.
    게임에서는 쓰기 어려울듯하지만 -_-

Leave a Reply