세마포어(Semaphore)를 이용한 블로킹(Blocking) 구현 원리 및 활용 예제 (C#)

이번 글에서는 세마포어(Semaphore)를 이용하여 블로킹(Blocking)을 구현하는 원리를 이해하고, 이를 활용한 대표적 사례로서 C#에서 블로킹 큐(Blocking Queue)를 구현하는 방법을 소개하도록 하겠습니다. 추가로 세마포어 기반 블로킹 방식을 채택할 때 일반적으로 고려해야 하는 성능적, 기능적 장단점에 대해서도 알아보겠습니다.


1. 세마포어를 활용한 블로킹 구현 원리

세마포어는 동시성 환경에서 스레드의 접근을 제어하기 위한 동기화 원시 객체(Primitive)이다. 세마포어의 주요 기능은 다음 두 가지로 정리할 수 있다.

  • Wait: 세마포어 카운트가 0보다 크면 카운트를 감소시키고 진입을 허용하며, 0이라면 호출한 스레드는 블로킹된다.
  • Release: 세마포어 카운트를 증가시키고, 블로킹 중인 스레드 하나의 대기를 해제한다.

즉, 세마포어는 특정 조건이 충족될 때까지 스레드를 차단(Block)했다가, 해당 조건 충족 시 차단을 해제하는 방식으로 블로킹을 구현하는 데 매우 적합하다.


2. 세마포어를 활용한 블로킹 큐 예제 (C#)

블로킹 큐는 큐가 비어있을 때 소비자 스레드를 블로킹하고, 아이템이 들어오면 자동으로 블로킹을 해제하여 소비자를 깨우는 구조를 가진 자료구조이다. 세마포어의 블로킹 기능을 잘 나타내는 대표적인 예시로, C#에서 아래와 같이 구현할 수 있다.

C# 세마포어 기반 블로킹 큐 예시 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Collections.Generic;
using System.Threading;

public class BlockingQueue<T>
{
private readonly Queue<T> queue = new Queue<T>();
private readonly SemaphoreSlim itemsAvailable = new SemaphoreSlim(0);
private readonly object lockObject = new object();

public void Enqueue(T item)
{
lock (lockObject)
{
queue.Enqueue(item);
}
itemsAvailable.Release(); // 아이템이 추가되었음을 알림
}

public T Dequeue()
{
itemsAvailable.Wait(); // 아이템이 없으면 여기서 블로킹됨
lock (lockObject)
{
return queue.Dequeue();
}
}
}

코드 핵심 설명

  • 큐가 비었을 때 소비자 스레드는 itemsAvailable.Wait() 메서드를 통해 블로킹 상태에 들어가며, CPU 자원을 낭비하지 않고 효율적으로 대기한다.
  • 생산자가 Enqueue로 아이템을 추가하면 itemsAvailable.Release()를 통해 대기 중인 소비자를 깨운다.

3. 세마포어 기반 블로킹 구현 시 고려해야 할 성능적, 기능적 장단점

세마포어를 기반으로 블로킹을 구현할 때는 다음과 같은 장단점을 반드시 고려해야 한다.

장점

  • 효율적 자원 관리: 스레드를 블로킹하여 CPU 자원 소모를 최소화할 수 있다.
  • 명확한 동기화: 블로킹 조건과 해제가 명시적이어서 코드의 흐름과 동작이 직관적이며 관리하기 쉽다.
  • 간단한 조건부 대기 구현: 특정 조건이 충족될 때까지 대기해야 하는 상황에서 코드 복잡성을 낮추고 명확한 로직으로 구현할 수 있다.

단점

  • 데드락(Deadlock)의 위험: 여러 세마포어를 조합할 경우, 신중하지 못한 순서 관리로 인해 데드락 발생 가능성이 존재한다.
  • 스타베이션(Starvation) 문제: 특정 스레드가 장시간 블로킹 상태에 갇혀 리소스를 할당받지 못할 위험이 있다.
  • 성능 저하 가능성: 과도한 세마포어 사용 및 빈번한 컨텍스트 전환(Context Switch)은 성능 저하를 유발할 수 있으므로, 대규모 동시성 환경에선 주의가 필요하다.

권장사항

  • 가능한 단순하고 명확한 로직에서만 세마포어 블로킹을 적용하는 것이 좋다.
  • 복잡한 조건부 블로킹 상황에서는 타임아웃(timeout)을 활용하여 예외상황 및 데드락을 방지한다.

4. 결론 및 요약

세마포어를 이용한 블로킹 구현은 스레드를 효율적으로 차단(Block)하고 해제(Release)하는 명시적인 동기화 방식이다. 이를 이용한 블로킹 큐 구현 예제를 통해 개념을 구체적으로 이해할 수 있었다.

그러나 세마포어를 사용할 때는 데드락과 스타베이션 같은 문제를 피하기 위한 세심한 설계가 필수이며, 특히 고도의 동시성 환경에서는 세마포어 사용 빈도와 컨텍스트 전환의 비용을 신중히 고려하여야 한다.