세마포어와 뮤텍스 차이, 언제 무엇을 쓸까
뮤텍스는 하나의 스레드만 자원에 접근하도록 잠그는 도구이고, 세마포어는 정해진 수만큼 동시 접근을 허용하는 신호 장치다. 두 도구의 차이와 실무에서 고르는 기준을 정리했다.
뮤텍스(mutex, 상호 배제 잠금)는 한 번에 스레드 하나만 공유 자원에 들어가도록 문을 잠그는 도구다. 세마포어(semaphore, 신호기)는 정해진 개수만큼 동시 접근을 허용하며 남은 자리를 세는 카운터다. 둘 다 여러 작업이 같은 자원을 동시에 건드려 생기는 충돌을 막지만, 뮤텍스는 한 명만 들이고 세마포어는 정해진 수만큼 들인다는 점이 다르다. 이 글은 운영체제 교과서에서 통상 설명되는 정의를 기준으로, 멀티스레드 환경에서 두 도구의 차이와 선택 기준을 다룬다. 개념 자체는 언어와 무관하므로, 동작을 확인하는 짧은 스위프트 예시만 곁들인다.
왜 잠금 장치가 필요한가
여러 스레드가 같은 변수나 파일을 동시에 고치면 결과가 실행 순서에 따라 달라진다. 이렇게 순서에 따라 값이 어긋나는 상황을 경쟁 상태(race condition)라고 부른다. 예를 들어 잔액이 만 원인 계좌에서 두 스레드가 동시에 잔액을 읽어 각각 오천 원을 더하면, 마지막에 쓴 값만 남아 한쪽 입금이 사라질 수 있다. 장부가 어긋나는 정도로 끝나지 않은 실제 사례도 있다. 낸시 레버슨(Nancy Leveson)과 클라크 터너(Clark Turner)가 1993년 IEEE Computer에 발표한 조사 보고서에 따르면, 방사선 치료기 테락-25(Therac-25)는 소프트웨어 경쟁 상태가 원인이 된 과다 피폭 사고를 1985년부터 1987년까지 6건 일으켰다.
이처럼 한 번에 하나의 스레드만 실행해야 하는 코드 구간을 임계 구역(critical section)이라고 한다. 뮤텍스와 세마포어는 바로 이 임계 구역을 지키는 대표적인 장치다. 둘 다 지금 들어가도 되는지를 확인하고 자리를 잡는 방식으로 충돌을 막지만, 허용하는 인원 수와 쓰임새가 갈린다.
잠금 없음 모드 — [다음 단계]를 눌러 본문의 계좌 예시(잔액 10,000원 + 5,000원 입금 둘)가 어떻게 어긋나는지 한 수씩 재현해 보자. 자동 데모는 두 모드를 이어서 보여준다.
뮤텍스는 어떻게 동작하나
뮤텍스는 잠금을 얻은 스레드 하나만 임계 구역에 들여보내고, 그 스레드가 나올 때 잠금을 푼다. 다른 스레드는 잠금이 풀릴 때까지 문 앞에서 기다린다. 이름 그대로 상호 배제(mutual exclusion), 곧 동시에 못 들어감을 강제하는 도구다.
뮤텍스의 중요한 성질로 소유권이 자주 언급된다. 잠금을 건 스레드만 그 잠금을 풀 수 있다는 규칙이다. 애플 공식 문서는 NSLock을 잠근 스레드가 아닌 다른 스레드에서 unlock을 호출하면 정의되지 않은 동작으로 이어질 수 있다고 명시하고, POSIX 표준도 오류 검사 타입(PTHREAD_MUTEX_ERRORCHECK) 뮤텍스에서 소유하지 않은 잠금을 푸는 pthread_mutex_unlock 호출에 EPERM 오류를 반환하도록 정의한다. 덕분에 지금 누가 이 자원을 쥐고 있는지가 분명해지고, 엉뚱한 스레드가 남의 잠금을 푸는 실수를 막기 쉽다.
- 보호 대상: 한 번에 한 스레드만 써야 하는 자원 하나
- 기본 동작: 잠금(lock) 뒤 작업, 끝나면 해제(unlock)
- 주의: 해제를 빠뜨리면 다른 스레드가 영영 기다린다
스위프트로 보면 이런 모양이다. 예외가 나도 잠금이 풀리도록 defer로 해제를 걸어 두는 것이 요령이다.
let lock = NSLock()
var balance = 10_000
func deposit(_ amount: Int) {
lock.lock()
defer { lock.unlock() }
balance += amount
}
세마포어는 무엇이 다른가
세마포어는 동시에 몇 개까지 들여보낼지를 숫자로 관리하는 신호 장치다. 안에 든 카운터가 남은 자리 수를 뜻해서, 스레드가 들어갈 때 하나 줄고 나올 때 하나 는다. 카운터가 0이면 자리가 빌 때까지 기다린다.
허용 인원이 1인 세마포어를 이진 세마포어(binary semaphore)라고 하며, 겉보기 동작은 뮤텍스와 비슷하다. 다만 세마포어는 소유권 개념 없이 신호를 세는 데 초점이 있어, 한 스레드가 신호를 보내고 다른 스레드가 그 신호를 받아 깨어나는 식의 순서 맞추기에도 쓴다. 작업을 만들어 넣는 스레드와 꺼내 쓰는 스레드 사이에서 물건이 하나 들어왔음을 알리는 용도가 대표적이다. 세마포어 개념은 컴퓨터 과학자 에츠허르 데이크스트라가 1965년 원고 '협력하는 순차 프로세스(Cooperating Sequential Processes)'에서 제안했고, 그가 붙인 두 연산 이름 P와 V가 오늘날 API의 wait와 signal에 그대로 대응한다.
- 계수 세마포어: 커넥션 풀처럼 동시 사용 개수를 제한할 때
- 이진 세마포어: 잠금 대신 스레드 사이 신호를 주고받을 때
- 카운터 값: 남은 허용 개수를 그대로 나타냄
동시 접근을 3개까지 허용하는 계수 세마포어는 DispatchSemaphore로 이렇게 쓴다.
let semaphore = DispatchSemaphore(value: 3)
DispatchQueue.concurrentPerform(iterations: 10) { _ in
semaphore.wait()
defer { semaphore.signal() }
// 동시에 최대 3개 스레드만 이 구간에 들어온다
}
언제 뮤텍스를 쓰고 언제 세마포어를 쓰나
자원 하나를 한 번에 한 스레드만 쓰도록 보호하는 게 목적이면 뮤텍스가 맞다. 반대로 동시 접근 수를 정해진 만큼 열어 주거나, 스레드끼리 준비됐다 같은 신호를 주고받아야 하면 세마포어가 어울린다. 판단이 헷갈릴 때는 몇 명을 들여보내고 싶은지와 잠그는 쪽과 푸는 쪽이 같은지를 먼저 따져 보면 방향이 잡힌다.
| 구분 | 뮤텍스 | 세마포어 |
|---|---|---|
| 동시 진입 | 스레드 1개 | 초기값 N개까지 |
| 소유권 | 잠근 스레드만 해제 | 없음 — 다른 스레드가 signal 가능 |
| 주 용도 | 공유 자원 하나 보호 | 동시 개수 제한, 스레드 간 신호 |
| 대표 API | NSLock, pthread_mutex | DispatchSemaphore, POSIX sem_t |
- 한 자원 보호, 잠근 스레드가 직접 해제: 뮤텍스
- 동시 접근 수 제한(예: 다운로드 3개까지): 계수 세마포어
- 생산자와 소비자 사이 신호 전달: 세마포어
동시 접근 수 제한은 플랫폼 기본 설정에서도 흔히 만난다. 애플 URLSessionConfiguration 문서에 따르면 호스트당 최대 동시 연결 수를 정하는 httpMaximumConnectionsPerHost의 기본값은 macOS 6개, iOS 4개다 — 이렇게 '한 번에 N개까지만'을 강제해야 하는 자리가 계수 세마포어의 자리다.
실무에서 자주 밟는 함정
둘 중 무엇을 쓰든 잠금을 다루는 코드에는 비슷한 위험이 따른다. 대표적인 것이 교착 상태(deadlock)로, 두 스레드가 서로가 쥔 잠금을 기다리며 둘 다 멈춰 버리는 상황이다. 에드워드 코프먼(E. G. Coffman Jr.) 연구진은 1971년 ACM Computing Surveys에 실린 논문 'System Deadlocks'에서 교착 상태의 필요조건 네 가지를 정리했는데, 그중 하나가 순환 대기(circular wait)다. 여러 잠금을 언제나 같은 순서로만 획득하면 이 순환 대기가 성립하지 않아 교착 상태가 원천적으로 차단된다. 어느 스레드가 어디서 멈췄는지 추적할 때는 로그가 절반을 해결한다 — os_log로 로깅을 옮기는 방법을 함께 봐 두면 좋다.
- 해제 누락: 예외가 나도 잠금이 풀리도록 코드를 감싼다
- 범위 최소화: 임계 구역은 꼭 필요한 줄만 짧게 잡는다
- 잠금 순서 고정: 여러 잠금은 정해진 순서로만 획득한다
정리하면 뮤텍스는 한 명만을 강제하는 잠금, 세마포어는 정해진 수를 관리하고 신호를 주고받는 카운터다. 보호할 자원이 하나이고 잠근 쪽이 직접 풀어야 하면 뮤텍스, 동시 허용 개수를 다루거나 스레드 사이 신호가 필요하면 세마포어를 먼저 떠올리면 대부분 맞아떨어진다. 서버 코드에서 이런 동시성 선택이 실제로 등장하는 맥락은 Vapor 서버 개발 글에서 볼 수 있다.
