[서버-3] 교착상태 방지를 위한 뮤텍스 잠금 순서 규칙

#서버 프로그래밍

교착상태(DeadLock) 방지를 위한 뮤텍스 잠금 순서 규칙


교착상태(DeadLock)

교착 상태(DeadLock)은 두 개 이상의 작업이 서로 상대방의 작업이 끝나는 것을 기다리는 상태로 멀티스레드 프로그래밍 환경에서 자주 발생한다.

다음과 같은 코드가 있다. 해당 코드는 데드락을 발생 시키는 코드이다. 무한 반복문(while)으로 애초에 끝나지 않는 프로그램이지만 어느 순간부터 콘솔 출력이 안될 것이다. 데드락이 발생한 것이다.

int main()
{
    // t1 스레드를 시작한다.
    thread t1([]()
    {
        while (1)
        {
            CriticalSectionLock lock(a_mutex);
            a++;
            CriticalSectionLock lock2(b_mutex);
            b++;
            cout << "t1 done.\n";
        }
    });
 
    // t2 스레드를 시작한다.
    thread t2([]()
    {
        while (1)
        {
            CriticalSectionLock lock(b_mutex);
            b++;
            CriticalSectionLock lock2(a_mutex);
            a++;
            cout << "t2 done.\n";
        }
    });
 
    // 스레드들의 일이 끝날 때까지 기다린다.
    // 사실상 무한 반복이므로 끝나지 않는다.
    t1.join();
    t2.join();
 
    return 0;
}

위 코드에서 아래와 같이 컨텍스트 스위치가 발생한다면 필연적으로 데드락이 발생한다. deadlock 게임 서버 프로그래밍 교과서

데드락이 발생한 서버는 아무런 작업을 할 수 없으므로 데드락이 생길 만한 상황을 최대한 만들지 않아야 한다.

그렇기 위해서는 잠금 순서의 규칙을 지키면 된다.

잠금 순서의 규칙

잠금의 해제 순서는 교착 상태에 영항을 주지 않는다. 해제는 마음대로 해도 된다. 중요한 것은 처음 잠금 순서이다!

교착 상태를 예방하려면 첫 번째 잠금 순서를 지켜야 한다(거꾸로 가지 말아야 한다).

교착 상태를 방지하기 위해서는 잠금 순서를 순서대로 지키면 된다. (거꾸로 잠금을 하면 교착이 발생할 상황이 생긴다.)

뮤텍스 A,B,C가 있다고 하자. 이들의 잠금 순서가 A -> B -> C 이라면 어느 스레드에서도 A -> B -> C 순서로 잠금을 해야된다.

만약 A -> C로 중간에 B를 생략하는 것은 괜찮다. 역순으로 가지만 않으면 된다. 즉, A -> C -> B는 역순이 생기므로 문제가 발생한다.

재귀 뮤텍스(Recursive Mutex)

뮤텍스는 재귀성을 가지는 것과 가지지 않는 것이 있다. 재귀 뮤텍스는 한 스레드가 뮤텍스를 여러 번 반복해서 잠그는 것을 원활하게 해준다.

예를 들어 스레드 1에서 뮤텍스 M의 lock()함수를 호출하여 잠갔는데 또 lock() 함수를 호출했다고 가정하자.

그 상황에서 unlock()함수가 호출된다면 잠금 해제 되는 것이 아니다. unlock() 이전에 총 두 번 잠갔으니 unlock()도 두 번 호출되어야 잠금이 풀린다.

lock(M)    // 잠금을 획득했다.
lock(M)    // 잠근 것을 또 잠갔다.
unlock(M)  // 잠금이 해제되었다. 그러나 아직 한 번 더 남았다.
unlock(M)  // 잠금이 해제되었다. 비로소 잠금 해제가 실질적으로 된다.

자, 이제 재귀 잠금을 보자 A -> B -> C -> B -> A 처음 세 번의 잠금으로 A, B, C가 잠겼는데 그 후로 B와 A를 또 잠갔다. 이런 상황이 재귀 잠금이다.

lock(A)    // ➊
lock(B)    // ➋
lock(C)    // ➌
lock(B)    // ➍
lock(A)    // ➎
unlock(C)  // ➏
unlock(B)  // ➐
unlock(A)  // ➑

➌~➎ 에서 잠근 순서가 역순으로 된다. 교착 상태가 발생하나? 아니다. 이미 잠금이 된 것은 순서에 신경을 쓰지 않아도 된다. 처음 잠금이 이루어 지는 것들에서 순서가 중요하다 !

lock(A)    // ➊
lock(C)    // ➋
lock(B)    // ➌
lock(C)    // ➍
lock(A)    // ➎
unlock(C)  // ➏
unlock(B)  // ➐
unlock(A)  // ➑

➊은 안전하다. ➋도 역시 안전하다 B를 건너 뛰었지만, 역순은 아니다. ➌ 에서 역순이 발생하여 교착 상태를 일으킨다. ➍, ➎는 재귀 잠금이므로 안전하다.

참고

게임 서버 프로그래밍 교과서