본문 바로가기
공부/네트워크

게임 서버 프로그래밍 - 교착 상태, 잠금 순서의 규칙

by 라이티아 2026. 9. 17.

7. 교착 상태

만약 도로 주행중 사거리에 도착했고, 나머지 3방향에 모두 차가 오고 있다면 과연 이후 차가 사거리를 빠져 나갈 수 있을까? 모두 먼저 갈려고 속도를 낸다면 모두 부딪쳐 버리거나 모두 멈춰서 움직이지 못할 것이다. 이것이 교착상태이다.

 

멀티스레드 프로그래밍에서 교착 상태라는 것은 두 스레드가 서로를 기다리게 되는 상황을 의미한다.

 

ThreadA()
{
    lock(a)
    {
        a++;
        lock(b)
        {
            b++
        }
    }
}

ThreadB()
{
    lock(b)
    {
        b++;
        lock(a)
        {
            a++;
        }
    }
}

위 코드에서 A, B 스레드가 동시에 실행될 경우 A는 b의 해제를 기다리고 B는 a의 해제를 기다리며 교착 상태가 만들어 진다.

그림 1. A가 a를, B가 b를 먼저 획득한 뒤 서로의 잠금 해제를 기다리는 상황

 

게임 서버에서 교착 상태가 되면 이하와 같은 일이 일어난다.

  1. CPU 사용량이 매우 낮거나 0%가 된다.
  2. 클라이언트가 서버를 사용할 수 없다. 서버에 요청을 보내도 반응이 없다.

교착 상태가 일어났을때 디버거로 원인을 찾을 수 있다.

 

윈도우에서 임계 영역 기능인 CRITICAL_SECTION 내용을 디버거로 확인하면 교착 상태가 어디에서 시작되었는지 알 수 있다.

  • 임계 영역 생성은 InitializeCriticalSectionEx로 한다. 이때 CRITICAL_SECTION 객체가 생성된다.
  • 임계 영역 제거는 DeleteCriticalSection으로 한다.
  • 임계 영역 잠금은 EnterCriticalSection으로 한다.
  • 임계 영역 잠금 해제는 LeaveCriticalSection으로 한다.

8. 잠금 순서의 규칙

여러 뮤텍스를 사용할 때에는 교착 상태를 예방하기 위해서 각 뮤텍스의 잠금 순서를 기억해 두어야 한다.

잠금을 할때는 순서를 확인하며 문제가 되는 지점이 있는지 확인해야 한다.

 

예를 들어 뮤텍스 A -> B -> C가 순서로 있을때에 코드로는 아래와 같다.

lock(A);
lock(B);
lock(C);
unlock(C);
unlock(B);
unlock(A);

 

이때에 순서를 꼬아서 A -> B, B -> C만, A -> C 로 처리 하는 것은 교착 상태를 일으키지 않는다. 기존의 순서를 어기지 않았기 때문이다.

 

하지만 B -> A, C -> A, C -> B와 같이 순 방향을 어길 경우 교착 상태가 벌어 질 수 있다.

그림 2. 현재 보유하지 않은 잠금은 모든 스레드가 정한 A → B → C 순서로 획득한다.

 

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

스레드 A가 lock(n)을 2번 했을 경우 2번 lock을 함을 기억하고, 이후 unlock(n)을 하더라도 2번중 1번만 unlock을 했다고 판정하고 여전히 lock 상태를 유지한다.

 

이때 재귀 뮤텍스의 경우 첫 잠금에서 순서를 지켰다면 이후 잠금에서는 문제가 되지 않는다.

 

  1. A -> B -> C -> B -> A
  2. A -> C -> B -> C- > A

이때 1의 경우는 이미 잠근 것에 대한 재귀 잠금 순서는 문제가 없기 때문에 문제가 되지 않는다. 하지만 2의 경우는 A -> C는 문제가 없지만 C -> B로 가면서 순서가 어긋나며 교착 위험이 생기게 된다.

 

다만 잠금 해제의 경우는 교착 상태에 영향을 주지 못한다. 즉, 교착 상태를 방지하려면 첫 잠금 순서를 지켜한 한다라는 것을 지켜야 한다.

 

참고 자료

그림 1·2는 본문의 예제를 바탕으로 직접 정리한 도식이다.