[서버-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();
}
위 코드는 다음과 같이 실행된다.
- 세마포어는 스레드 2개만 접근을 허락한다.
- 스레드 3개가 접근을 요청한다.
- 스레드 2개만 접근을 허가받고 나머지를 실행한다.
- 일을 마친 스레드는 세마포어에 접근 끝났음을 통보한다.
- 접근이 끝난 스레드가 나오면 대기 중인 스레드가 접근을 허가 받는다.
윈도우에는 다음과 같은 세마포어 함수가 있다.
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 감소한다. 이런 식으로 세마포어를 활용할 수 있다.