에듀테크 프로젝트 대시보드
세마포어 (생산자-소비자)
한정된 크기의 버퍼를 여러 스레드가 동시에 채우고 비울 때 데이터가 깨지지 않게 하는 고전 동기화 문제 · Dijkstra, 1965
생산자 스레드는 데이터를 만들어 공유 버퍼(큐)에 넣고, 소비자 스레드는 버퍼에서 꺼내 처리한다.
유튜브 영상 재생(디코더가 채우고 사운드/화면이 소비), 웹 서버(수신 스레드가 요청을 큐에 넣고 워커가 처리),
프린터 스풀러 — 전부 이 구조다. 그런데 버퍼는 크기가 유한하고, 여러 스레드가 같은 변수
(버퍼 배열, count, 쓰기 위치)를 건드린다.
count++ 를 해서 한 번의 증가가 사라진다
(잃어버린 갱신).세마포어는 0 이상의 정수 하나와, 그 값이 0일 때 잠들어 기다리는 스레드들의 대기 큐로 이루어진 객체다. 연산은 딱 두 개, 그리고 둘 다 원자적(중간에 다른 스레드가 끼어들 수 없음)이다.
값이 1과 0만 오가면 이진 세마포어(잠금과 비슷)이고, N까지 셀 수 있으면
카운팅 세마포어다. 뮤텍스와 달리 V를 부른 스레드와 P를 부른 스레드가 달라도 된다 —
이 성질 덕분에 "한쪽이 만들면 반대쪽을 깨운다"는 신호 전달에 딱 맞는다.
버퍼 크기를 N이라 할 때 세마포어 세 개를 둔다.
| 세마포어 | 초깃값 | 뜻 | 누가 P / 누가 V |
|---|---|---|---|
empty | N | 비어 있는 칸 수 | 생산자가 P (칸 하나 확보) · 소비자가 V (칸 하나 반납) |
full | 0 | 채워진 칸 수 (읽을 거리) | 소비자가 P (읽을 것 확보) · 생산자가 V (읽을 것 추가) |
mutex | 1 | 버퍼 자료구조 자체의 잠금 | 둘 다 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가 없어서
버퍼에 넣지도 못하는 데드락이 된다. 아래 시뮬레이터에서 직접 켜 볼 수 있다.
생산자·소비자가 각자의 속도로 돌아간다. 세마포어 값, 버퍼 상태, 블로킹을 실시간으로 본다.
· 생산자를 소비자보다 빠르게 하면 버퍼가 가득 차서 생산자가 블록된다 —
전체 처리량은 결국 느린 쪽 속도로 수렴한다(백프레셔).
· "데드락 순서" 버튼을 누르면 곧 양쪽이 서로를 기다리며 영원히 멈춘다.
| 뮤텍스 (Mutex) | 세마포어 (Semaphore) | 조건 변수 (Condition Var) | |
|---|---|---|---|
| 본질 | 0/1 잠금 + 소유자 | 정수 카운터 + 대기 큐 | 대기/통지 신호 (락과 짝) |
| 소유 개념 | 있음 — 잠근 스레드만 푼다 | 없음 — 아무나 V 가능 | 없음 |
| 주 용도 | 임계 구역 상호 배제 | 자원 N개 관리 · 스레드 간 신호 | "어떤 조건이 될 때까지" 대기 |
| 유한 버퍼 | 버퍼 잠금용으로 1개 | empty·full 로 칸 수 추적 | notEmpty·notFull 두 개로 대체 가능 |
현대 코드는 세마포어 대신 락 + 조건 변수(Java BlockingQueue, Python queue.Queue,
Go 채널)로 같은 문제를 푸는 경우가 많다. 원리는 똑같다: "넣을 공간"과 "꺼낼 내용"이라는 두 조건을 기다린다.
V(mutex)를 안 하면 다음 스레드가 영원히 못 들어온다. 예외 발생 경로도 확인.while로 조건을 다시 검사해야 한다(가짜 기상 spurious wakeup).