한양과학기술고등학교

에듀테크 프로젝트 대시보드

세마포어 (생산자-소비자)

세마포어와 생산자-소비자 문제

한정된 크기의 버퍼를 여러 스레드가 동시에 채우고 비울 때 데이터가 깨지지 않게 하는 고전 동기화 문제 · Dijkstra, 1965

1. 왜 문제가 되는가

생산자 스레드는 데이터를 만들어 공유 버퍼(큐)에 넣고, 소비자 스레드는 버퍼에서 꺼내 처리한다. 유튜브 영상 재생(디코더가 채우고 사운드/화면이 소비), 웹 서버(수신 스레드가 요청을 큐에 넣고 워커가 처리), 프린터 스풀러 — 전부 이 구조다. 그런데 버퍼는 크기가 유한하고, 여러 스레드가 같은 변수 (버퍼 배열, count, 쓰기 위치)를 건드린다.

아무 보호 없이 돌리면 세 가지가 깨진다.
  • 버퍼 오버플로 — 가득 찬 버퍼에 생산자가 계속 써서 아직 안 읽은 데이터를 덮어쓴다.
  • 언더플로 — 빈 버퍼에서 소비자가 꺼내 쓰레기 값을 읽는다.
  • 경쟁 상태 — 두 생산자가 동시에 count++ 를 해서 한 번의 증가가 사라진다 (잃어버린 갱신).

2. 세마포어 = 정수 카운터 + 대기줄

세마포어는 0 이상의 정수 하나와, 그 값이 0일 때 잠들어 기다리는 스레드들의 대기 큐로 이루어진 객체다. 연산은 딱 두 개, 그리고 둘 다 원자적(중간에 다른 스레드가 끼어들 수 없음)이다.

P / wait / down
−1
값을 1 줄인다. 줄인 결과가 음수면 블록되어 대기 큐로.
V / signal / up
+1
값을 1 늘린다. 기다리던 스레드가 있으면 하나를 깨운다.
의미
≥ 0
"지금 쓸 수 있는 자원의 개수". 음의 크기 = 대기 중인 스레드 수.

값이 1과 0만 오가면 이진 세마포어(잠금과 비슷)이고, N까지 셀 수 있으면 카운팅 세마포어다. 뮤텍스와 달리 V를 부른 스레드와 P를 부른 스레드가 달라도 된다 — 이 성질 덕분에 "한쪽이 만들면 반대쪽을 깨운다"는 신호 전달에 딱 맞는다.

3. 세마포어 3개로 푸는 유한 버퍼

버퍼 크기를 N이라 할 때 세마포어 세 개를 둔다.

세마포어초깃값뜻누가 P / 누가 V
emptyN비어 있는 칸 수생산자가 P (칸 하나 확보) · 소비자가 V (칸 하나 반납)
full0채워진 칸 수 (읽을 거리)소비자가 P (읽을 것 확보) · 생산자가 V (읽을 것 추가)
mutex1버퍼 자료구조 자체의 잠금둘 다 P 했다가 V (임계 구역 보호)
# 생산자
while True:
    item = produce()
    P(empty)          # 빈 칸이 없으면 여기서 잠듦
    P(mutex)          # 버퍼 건드리기 직전에만 잠금
    buffer.append(item)
    V(mutex)
    V(full)           # "읽을 거 하나 생겼다"고 소비자에게 신호

# 소비자
while True:
    P(full)           # 읽을 게 없으면 여기서 잠듦
    P(mutex)
    item = buffer.pop(0)
    V(mutex)
    V(empty)          # "빈 칸 하나 났다"고 생산자에게 신호
    consume(item)
순서가 생명이다. 자원 세마포어(empty/full)를 먼저 P 하고 그다음 mutex를 P 해야 한다. 반대로 하면 — 소비자가 mutex를 쥔 채 full을 기다리며 잠들고, 생산자는 그 mutex가 없어서 버퍼에 넣지도 못하는 데드락이 된다. 아래 시뮬레이터에서 직접 켜 볼 수 있다.

4. 시뮬레이터

생산자·소비자가 각자의 속도로 돌아간다. 세마포어 값, 버퍼 상태, 블로킹을 실시간으로 본다.

버퍼 N 5
생산자 속도 보통
소비자 속도 보통
🏭 생산자
대기
→
🛒 소비자
대기
empty
5
빈 칸
full
0
읽을 거리
mutex
1
버퍼 잠금
생산 0 소비 0 생산자 블록 0틱 소비자 블록 0틱

· 생산자를 소비자보다 빠르게 하면 버퍼가 가득 차서 생산자가 블록된다 — 전체 처리량은 결국 느린 쪽 속도로 수렴한다(백프레셔).
· "데드락 순서" 버튼을 누르면 곧 양쪽이 서로를 기다리며 영원히 멈춘다.

5. 세마포어 vs 뮤텍스 vs 조건 변수

뮤텍스 (Mutex)세마포어 (Semaphore)조건 변수 (Condition Var)
본질0/1 잠금 + 소유자정수 카운터 + 대기 큐대기/통지 신호 (락과 짝)
소유 개념있음 — 잠근 스레드만 푼다없음 — 아무나 V 가능없음
주 용도임계 구역 상호 배제자원 N개 관리 · 스레드 간 신호"어떤 조건이 될 때까지" 대기
유한 버퍼버퍼 잠금용으로 1개empty·full 로 칸 수 추적notEmpty·notFull 두 개로 대체 가능

현대 코드는 세마포어 대신 락 + 조건 변수(Java BlockingQueue, Python queue.Queue, Go 채널)로 같은 문제를 푸는 경우가 많다. 원리는 똑같다: "넣을 공간"과 "꺼낼 내용"이라는 두 조건을 기다린다.

6. 흔한 함정