[서버-5] `Mutex` vs `Semaphore`

#서버 프로그래밍

뮤텍스와 세마포어의 차이점 이해하기


세마포어?

뮤텍스오직 1개의 스레드만 자원에 접근할 수 있도록 하지만 세마포어는 원하는 개수의 스레드가 자원에 접근할 수 있도록 한다.

다음과 같은 코드를 예시로 보면

Semaphore sema1;
 
void Main()
{
  // 스레드 2개만 자원을 액세스할 수 있게 제한한다.
  sema1 = new Semaphore(2);
}
 
void Thread1()
{
  // 리소스를 액세스할 수 있을 때까지 기다린다.
  sema1.Wait();
 
  // 리소스 액세스가 다 끝났음을 세마포어에 알린다.
  sema1.Release();
}
 
void Thread2()
{
  // 리소스를 액세스할 수 있을 때까지 기다린다.
  sema1.Wait();
 
  // 리소스 액세스가 다 끝났음을 세마포어에 알린다.
  sema1.Release();
}
 
void Thread3()
{
  // 리소스를 액세스할 수 있을 때까지 기다린다.
  sema1.Wait();
 
  // 리소스 액세스가 다 끝났음을 세마포어에 알린다.
  sema1.Release();
}

위 코드는 다음과 같이 실행된다.

  1. 세마포어는 스레드 2개만 접근을 허락한다.
  2. 스레드 3개가 접근을 요청한다.
  3. 스레드 2개만 접근을 허가받고 나머지를 실행한다.
  4. 일을 마친 스레드는 세마포어에 접근 끝났음을 통보한다.
  5. 접근이 끝난 스레드가 나오면 대기 중인 스레드가 접근을 허가 받는다.

윈도우에는 다음과 같은 세마포어 함수가 있다.

  • CreateSemaphore : 세마포어를 만든다. 이 때 접근 가능한 스레드의 개수도 설정한다.
  • WaitForSingleObject : 자원 액세스를 요청하고, 허락할 때까지 기다린다.
  • ReleaseSemaphore : 세마포어에 자원 액세스가 끝났음을 통보한다.
  • CloseHandle : 세마포어를 파괴한다.

세마포어가 자원 접근을 1개의 스레드에만 허락한다면 이는 뮤텍스와 다를 바가 없다.

세마포어와 이벤트

세마포어는 상태 값을 가지고 있다. 그 값은 0 이상의 정수이고 초깃값은 앞서 설정했던 접근 가능한 최대 스레드의 개수이다.

스레드가 세마포어에 자원 액세스를 요청하면 상태 값은 1 감소한다. 세마포어의 상태 값이 0이 되면 더 이상 요청을 허락하지 않는다.

상태 값이 0인 상태에서 요청을 날린 스레드는 잠에 든다.

스레드가 작업을 마쳐 세마포어에 액세스 종료 통보를 날리면 상태 값이 1 증가한다. 1이 증가한 시점에서 세마포어는 잠 자면서 대기 중이면 스레드를 깨운다.

세마포어는 이벤트와 비슷하지만 이벤트는 0 혹은 1 두 개의 값만 가진다면 세마포어는 0 이상의 아무 정수 값을 가질 수 있다.

여기서 세마포어의 초기값을 1로 설정한다면 사실상 이벤트와 동일하다.

세마포어의 또 다른 용도

이러한 세마포어의 특성을 활용하면 세마포어를 유용하게 활용할 수 있다.

두 스레드간의 공유하는 큐(queue)가 있다고 가정하자. 한 스레드는 큐에서 항목을 꺼내고, 큐가 비어 있으면 무언가가 들어올 때까지 잠을 잔다. 나머지 스레드는 큐에 항목을 넣는 역할을 한다.

이벤트 기반의 코드는 다음과 같다.

Queue queue;
Event queueIsNotEmpty;
 
void Thread1()
{
  while (true)
  {
      queueIsNotEmpty.Wait();
      queue.PopFront();
  }
}
 
void Thread2()
{
  while (true)
  {
      queue.PushBack();
      queueIsNotEmpty.SetEvent();
  }
}

위 코드는 스레드 1·2가 번갈아 일을 한다면 문제가 없다. 그러나 생길 수 있는 문제점은 스레드 2가 스레드 1보다 빠르게 작동될 경우이다.

이벤트는 상태 값이 0 아니면 1 둘 중 하나이지만 큐 push를 연속으로 두 번 한다고 가정하면 큐에 크기는 2이고 상태는 1이다.

이어서 큐 pop을 하면 상태는 0이지만 크기는 1이다. 큐에 항목이 있음에도 스레드는 상태 값으로 인해 무한 대기에 걸린다.

이를 해결하려면 상태 값이 1 이상의 값을 가질 수 있어야 한다. 이때 사용할 수 있는 것이 세마포어이다.

Queue queue;
Semaphore queueIsNotEmpty;
 
void Main()
{
  // 초깃값이 0인 세마포어를 만든다.
  queueIsNotEmpty = new Semaphore(0);
}
 
void Thread1()
{
  while (true)
  {
      queueIsNotEmpty.Wait();
      queue.PopFront();
  }
}
 
void Thread2()
{
  while (true)
  {
      queue.PushBack();
      queueIsNotEmpty.Release();
  }
}

스레드 2에서 큐 push를 한 다음 자원 액세스 종료를 통보하는 Release()를 호출하면 상태 값이 1 증가한다. 세마포어이므로 1 이상의 값도 가질 수 있다.

스레드 1에서 상태 값이 0이면 대기한다. 상태 값이 1이 되면 접근을 하고 상태 값은 1 감소한다. 이런 식으로 세마포어를 활용할 수 있다.

참고

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