세마포어와 뮤텍스, 동기화 목적에 따른 선택
세마포어와 뮤텍스의 카운팅, 소유권, 재귀 획득, 우선순위 상속 차이와 동기화 상황별 선택 기준을 정리한다.
2026-08-14 · 최초 발행 2026-01-04
동기화에서 서로 다른 책임을 맡는다
세마포어와 뮤텍스는 모두 동시 실행 중인 작업을 조율하지만, 같은 문제를 같은 방식으로 풀지는 않는다. 세마포어는 신호와 자원 개수를 표현하는 범용 도구이고, 뮤텍스는 하나의 공유 자원에 대한 상호배제에 초점을 둔다.
가장 큰 차이는 소유권이다. 세마포어는 P/V 연산을 호출한 주체가 반드시 같을 필요가 없지만, 뮤텍스는 잠금을 획득한 쓰레드가 해제 책임도 가진다. 이 차이가 자원 풀 관리, 작업 순서 보장, 단순한 임계 영역 보호에서 도구를 가르는 기준이 된다.
세마포어는 0 이상의 정수 카운터를 사용하며, 상호배제뿐 아니라 순서 동기화와 자원 관리에도 쓸 수 있다. 뮤텍스는 Locked(1)와 Unlocked(0)의 이진 상태로 동작하며, 상호배제만을 제공한다.
| 특성 | Semaphore | Mutex |
|---|---|---|
| 값 범위 | 0 이상 정수 | 0(unlocked) / 1(locked) |
| 초기값 | 자원 개수 | 1 (unlocked) |
| 의미 | 사용 가능한 자원 수 | 잠금 여부 |
| 유형 | Binary / Counting | Binary만 |
// Semaphore: 프린터 5대 관리
Semaphore printers = 5; // 0~5 범위
// Mutex: 단일 자원 보호
Mutex critical_section = UNLOCKED; // 0 또는 1
소유권이 필요한 잠금인가
세마포어는 한 쓰레드가 획득한 뒤 다른 쓰레드가 해제할 수 있다. 신호를 보내고 받는 협력 구조에는 유용하지만, 누가 자원을 해제해야 하는지 강제하지는 않는다.
// Thread A
P(&sem); // 세마포어 획득
// Thread B
V(&sem); // Thread A가 아닌 B가 해제 가능!
뮤텍스는 반대다. 잠금을 가진 쓰레드만 해제할 수 있으므로, 공유 자원 보호에서 책임 범위를 명확하게 만든다.
// Thread A
mutex_lock(&mtx); // 뮤텍스 획득
// Thread B
mutex_unlock(&mtx); // 오류! A만 해제 가능
세마포어는 프로세스 간 신호 전달과 협력에 맞고, 뮤텍스는 특정 쓰레드가 자원을 보호하는 상황에 맞는다.
카운팅과 신호가 필요한 경우
세마포어는 상호배제 외에도 순서 동기화, 자원 카운팅, 이벤트 알림에 사용할 수 있다. 뮤텍스의 용도는 상호배제에 한정된다.
작업 A가 끝난 뒤에만 작업 B가 실행되어야 한다면, 카운터를 0으로 시작한 세마포어를 신호로 쓸 수 있다.
// 순서 동기화
Semaphore sync = 0;
void thread1() {
do_work_A();
V(&sync); // Thread 2에게 신호
}
void thread2() {
P(&sync); // Thread 1 대기
do_work_B(); // A 이후 실행 보장
}
공유 변수처럼 하나의 자원만 보호하려는 경우에는 뮤텍스가 더 직접적이다.
// 상호배제만
Mutex mtx;
void critical_section() {
mutex_lock(&mtx);
shared_resource++; // 보호된 접근
mutex_unlock(&mtx);
}
재귀 호출과 우선순위 처리
Binary Semaphore는 같은 실행 흐름에서 다시 획득하려 하면 값이 감소하면서 Deadlock에 빠질 수 있다.
// Binary Semaphore: 재귀 불가
void recursive_sem() {
P(&sem); // 첫 호출: 성공
P(&sem); // 두 번째: Deadlock!
V(&sem);
V(&sem);
}
Recursive Mutex는 같은 쓰레드가 여러 번 잠금을 획득할 수 있다.
// Recursive Mutex
pthread_mutexattr_t attr;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, &attr);
void recursive_func(int depth) {
pthread_mutex_lock(&mtx); // 동일 쓰레드는 여러 번 획득 가능
if (depth > 0)
recursive_func(depth - 1);
pthread_mutex_unlock(&mtx);
}
세마포어는 소유권 개념이 없어 우선순위 상속을 제공하지 못하며, 우선순위 역전 문제가 생길 수 있다. 뮤텍스는 Priority Inheritance를 통해 이 문제를 완화할 수 있어 실시간 시스템에서 중요하다.
// Priority Ceiling Protocol
pthread_mutexattr_t attr;
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, &attr);
// 낮은 우선순위 쓰레드가 Lock 보유 시
// 높은 우선순위 쓰레드 대기 중이면
// → 낮은 쓰레드의 우선순위가 일시적으로 상승
Binary Semaphore가 뮤텍스를 대체하지 못하는 이유
Binary Semaphore를 사용하면 임계 영역을 뮤텍스와 비슷하게 만들 수 있다.
// Binary Semaphore
Semaphore sem = 1;
// Mutex처럼 사용 (하지만 소유권 검사 없음)
void critical_section() {
P(&sem);
// Critical Section
V(&sem);
}
하지만 다른 쓰레드도 V()를 호출할 수 있고, 재귀 획득과 우선순위 상속도 지원하지 않는다. 상호배제만을 명확하게 구현하려면 뮤텍스가 제공하는 소유권 검사가 더 적합하다.
POSIX 뮤텍스는 오류 검출 속성을 통해 같은 쓰레드의 중복 Lock이나 소유하지 않은 쓰레드의 Unlock을 확인할 수 있다.
// POSIX Mutex with Error Checking
pthread_mutexattr_t attr;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, &attr);
// 같은 쓰레드가 다시 Lock 시도
pthread_mutex_lock(&mtx);
int ret = pthread_mutex_lock(&mtx);
if (ret == EDEADLK) {
printf("Deadlock detected!\n"); // 검출 가능
}
// 다른 쓰레드가 Unlock 시도
ret = pthread_mutex_unlock(&mtx);
if (ret == EPERM) {
printf("Not owner!\n"); // 오류 검출
}
경쟁 여부에 따라 달라지는 비용
세마포어는 카운팅과 큐 관리를 수행하고, 뮤텍스는 이진 상태와 소유자 정보를 다룬다.
| 측면 | Semaphore | Mutex |
|---|---|---|
| 구현 복잡도 | 높음 (카운팅, 큐 관리) | 낮음 (이진 상태) |
| 메모리 사용 | 큐 + 카운터 | 플래그 + 소유자 ID |
| Fast Path | P/V 원자 연산 | Lock/Unlock CAS |
| Slow Path | 큐 관리 + 스케줄링 | 대기 큐 + 우선순위 처리 |
Linux의 Futex 기반 뮤텍스는 경쟁이 없을 때 커널에 들어가지 않고 유저 공간에서 처리한다.
// Linux Futex 기반 Mutex
// Uncontended case: 커널 개입 없이 유저 공간에서 처리
int mutex_lock_fast(int *futex) {
if (__sync_bool_compare_and_swap(futex, 0, 1)) {
return 0; // Fast path: 즉시 획득
}
// Slow path: 커널 호출
return syscall(SYS_futex, futex, FUTEX_WAIT, 1, NULL);
}
경쟁이 없을 때는 단순한 CAS를 사용하는 뮤텍스가 더 빠르며, 경쟁이 발생하면 둘 다 커널이 개입하므로 비슷하다.
자원 풀과 임계 영역에 적용하기
제한된 자원의 수를 관리하거나 생산자-소비자 구조, 이벤트 알림을 구현할 때는 세마포어가 자연스럽다.
// 1. 제한된 자원 풀 관리
Semaphore db_connections = 10;
void query_database() {
P(&db_connections); // 연결 획득
execute_query();
V(&db_connections); // 연결 반환
}
// 2. 생산자-소비자
Semaphore items = 0;
Semaphore spaces = BUFFER_SIZE;
void producer() {
P(&spaces);
produce_item();
V(&items);
}
void consumer() {
P(&items);
consume_item();
V(&spaces);
}
// 3. 이벤트 알림
Semaphore event = 0;
void waiter() {
P(&event); // 이벤트 대기
handle_event();
}
void signaler() {
trigger_event();
V(&event); // 대기자에게 알림
}
단순한 공유 데이터 보호, 재귀 호출, 우선순위가 중요한 실시간 시스템에는 뮤텍스가 맞는다.
// 1. 단순 상호배제
Mutex lock;
void update_shared_data() {
mutex_lock(&lock);
shared_counter++; // 보호된 접근
mutex_unlock(&lock);
}
// 2. 재귀 호출
Mutex recursive_lock; // PTHREAD_MUTEX_RECURSIVE
void recursive_function(int depth) {
mutex_lock(&recursive_lock);
if (depth > 0)
recursive_function(depth - 1);
mutex_unlock(&recursive_lock);
}
// 3. 우선순위 중요한 실시간 시스템
Mutex rt_lock; // PTHREAD_PRIO_INHERIT
void high_priority_task() {
mutex_lock(&rt_lock); // 우선순위 상속 보장
critical_work();
mutex_unlock(&rt_lock);
}
운영체제 API에서 드러나는 차이
Linux Kernel은 세마포어에 카운터와 대기 목록을 두고, 뮤텍스에는 소유자와 대기 목록을 둔다.
// Semaphore
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
// Mutex (더 가벼움)
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
struct list_head wait_list;
};
Linux Documentation은 단순 잠금에는 더 엄격한 의미론과 더 나은 디버깅 지원을 갖는 뮤텍스를 세마포어보다 우선하라고 권장한다.
"Mutexes are preferred over semaphores for simple locking, as they have stricter semantics and better debugging support."
POSIX에서는 세마포어와 뮤텍스를 각각 별도 API로 제공한다.
// Semaphore
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 1);
sem_wait(&sem);
sem_post(&sem);
// Mutex
#include <pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mtx);
pthread_mutex_unlock(&mtx);
Windows도 세마포어와 뮤텍스를 구분하며, 프로세스 내부에서는 Critical Section을 사용할 수 있다.
// Semaphore
HANDLE hSem = CreateSemaphore(NULL, 5, 5, NULL);
WaitForSingleObject(hSem, INFINITE);
ReleaseSemaphore(hSem, 1, NULL);
// Mutex
HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
WaitForSingleObject(hMutex, INFINITE);
ReleaseMutex(hMutex);
// Critical Section (더 빠름, 프로세스 내부)
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
필요한 동기화 의미부터 고르기
카운팅이 필요한지, 단순 상호배제인지, 순서 동기화나 신호 전달인지에 따라 선택이 달라진다.
공유 자원 보호, 재귀적 Lock, 우선순위 상속, 디버깅 지원, 단순한 경우의 성능이 필요하면 뮤텍스를 선택한다. 제한된 자원 풀, 생산자-소비자 패턴, 이벤트 알림, 순서 동기화, 카운팅이 필요한 경우에는 세마포어를 선택한다.