동시에 접근 가능한 개수를 세는 카운터. 값이 0이면 대기, 0보다 크면 통과하며 하나 줄인다.
동작
-
Semaphore(3) — ← 자원 3개
-
acquire() — → 카운터 3 → 2, 통과
-
acquire() — → 2 → 1, 통과
-
acquire() — → 1 → 0, 통과
-
acquire() — → 0 이므로 대기
-
release() — → 0 → 1, 대기하던 하나가 깨어남
주차장 입구의 남은 자리 표시판과 같다. 0이면 나올 때까지 기다린다.
두 종류
| 값 | 쓰임 | |
|---|---|---|
| 이진 세마포어 | 0 또는 1 | 상호 배제(뮤텍스와 비슷하지만 소유권 없음) |
| 카운팅 세마포어 | 0 이상 | 동시 접근 수 제한 |
뮤텍스와 무엇이 다른가 — 소유권과 목적
| 뮤텍스 | : "내가 쓰는 동안 아무도 못 들어와" | → 자원 보호 |
|---|---|---|
| 세마포어 | : "동시에 3명까지" | → 개수 제한 / 신호 전달 |
세마포어는 소유권이 없어 A가 acquire 하고 B가 release 할 수 있다. 이 성질 때문에 순서 제어(신호 보내기)에 쓸 수 있다.
Semaphore ready = new Semaphore(0); // 0에서 시작
// 스레드 A (소비자)
ready.acquire(); // 준비될 때까지 대기
process();
// 스레드 B (생산자)
prepare();
ready.release(); // "준비 끝났다"고 신호 ← 잠근 적 없는데 푼다
뮤텍스로는 이런 사용이 불가능하다(자기가 잠근 것만 풀 수 있으므로).
실무에서 가장 흔한 용도 — 동시 실행 수 제한
Semaphore limit = new Semaphore(10); // 외부 API 동시 호출 10개까지
public Response call() throws InterruptedException {
limit.acquire();
try {
return externalApi.call();
} finally {
limit.release(); // finally 필수 — 안 풀면 영구 고갈
}
}
왜 필요한가 — 외부 시스템이 감당할 수 있는 동시 요청 수는 한계가 있다. 스레드 풀 크기와 별개로 특정 자원에 대한 동시성만 제한하고 싶을 때 쓴다. DB 커넥션 풀도 본질적으로 세마포어다.
tryAcquire(100, MILLISECONDS) 로 빨리 실패시키면
대기 폭증(bulkhead 붕괴)을 막을 수 있다.
고전 문제
- 생산자-소비자 —
empty(빈 칸 수),full(찬 칸 수) 두 세마포어 + 뮤텍스 - 식사하는 철학자 — 포크를 세마포어로 표현. 전원이 왼쪽부터 집으면 데드락
함정
release를 빠뜨리면 자원이 영구히 줄어든다. 예외 경로에서 특히 잘 발생한다.
뮤텍스의 synchronized처럼 자동 해제해 주는 문법이 없으므로
try-finally가 필수다.
반대로 release를 더 많이 부르면 카운터가 초기값을 넘어 제한이 무너진다.