동시성 제어에서 뮤텍스·세마포어·모니터와 락 선택하기

동시성 제어에서 뮤텍스, 세마포어, 모니터, Spinlock, Read-Write Lock의 동작과 선택 기준을 정리한다.

2026-08-14 · 최초 발행 2026-01-16

공유 자원 접근에서 먼저 막아야 할 문제

여러 프로세스나 스레드가 함께 실행될 때 공유 자원에 접근하면 실행 순서에 따라 결과가 달라질 수 있다. 동시성 제어는 이 접근을 조율해 병렬 실행을 안전하게 만드는 기술이다.

YesYesYesYesNo공유 자원 접근동기화 없음?경쟁 조건Race Condition데이터 불일치Data Inconsistency교착 상태Deadlock기아 상태Starvation안전한 접근
문제 설명 예시
경쟁 조건 실행 순서에 따른 결과 차이 동시 카운터 증가
데이터 불일치 공유 데이터의 무결성 훼손 부분 업데이트
교착 상태 상호 대기로 인한 영구 블로킹 순환 락 대기
기아 상태 특정 프로세스의 무한 대기 우선순위 역전

공유 자원에 실제로 접근하는 코드를 임계 영역(Critical Section)이라 한다. 이 영역에는 한 번에 하나의 프로세스만 들어갈 수 있어야 하며, 다음 조건을 만족해야 한다.

요구사항 설명
상호 배제 한 프로세스가 임계 영역 내에 있으면 다른 프로세스 진입 불가
진행 임계 영역이 비어있으면 대기 중인 프로세스 진입 가능
한정 대기 임계 영역 진입 대기 시간이 유한해야 함

뮤텍스는 소유권으로 임계 영역을 보호한다

뮤텍스(Mutex, Mutual Exclusion)는 상호 배제를 위한 기본 동기화 도구다. 락을 획득한 스레드만 임계 영역에 진입하고, 그 스레드만 락을 해제할 수 있다.

임계 영역스레드 2뮤텍스스레드 1임계 영역스레드 2뮤텍스스레드 1lock() 요청락 획득임계 영역 진입lock() 요청대기 (블로킹)작업 수행unlock()락 획득임계 영역 진입
특성 설명
소유권 락을 획득한 스레드만 해제 가능
바이너리 락/언락 두 가지 상태만 존재
블로킹 락 획득 실패 시 대기 상태
재귀적 Recursive Mutex는 동일 스레드의 중복 획득 허용

플랫폼마다 API 표기는 다르지만, 락 획득·해제와 비차단 획득이라는 역할은 같다.

언어/플랫폼 Lock Unlock Try Lock
POSIX pthread_mutex_lock() pthread_mutex_unlock() pthread_mutex_trylock()
C++ mutex.lock() mutex.unlock() mutex.try_lock()
Java synchronized / lock() 자동 / unlock() tryLock()
Windows WaitForSingleObject() ReleaseMutex() WaitForSingleObject(0)

뮤텍스는 구현이 단순하고 소유권 개념으로 안전성을 제공한다. 반면 단일 자원 보호에 적합하며, 우선순위 역전과 데드락 위험은 남는다.

세마포어는 자원 수와 대기를 함께 관리한다

세마포어(Semaphore)는 정수 값을 가진 동기화 도구다. 여러 프로세스나 스레드가 공유 자원에 접근하는 상황을 제어하며, Dijkstra가 제안한 P(wait)와 V(signal) 연산으로 조작한다.

세마포어이진 세마포어Binary Semaphore계수 세마포어Counting Semaphore값: 0 또는 1뮤텍스와 유사값: 0 ~ N다중 자원 관리
유형 값 범위 용도
이진 세마포어 0 또는 1 상호 배제, 뮤텍스 대용
계수 세마포어 0 ~ N 다중 자원 풀 관리

P 연산은 자원이 उपलब्ध할 때까지 기다린 뒤 값을 감소시키고, V 연산은 값을 증가시켜 필요하면 대기 중인 프로세스를 깨운다.

wait(S) {
    while (S <= 0)
        ; // 대기 (busy waiting 또는 block)
    S--;
}
signal(S) {
    S++;
    // 대기 중인 프로세스 깨우기 (있다면)
}

초기값이 2인 세마포어에서는 두 프로세스가 wait()를 수행한 뒤 값이 0이 된다. 이후 요청한 프로세스는 signal()이 호출될 때까지 대기한다.

프로세스 3프로세스 2세마포어 (초기값:2)프로세스 1프로세스 3프로세스 2세마포어 (초기값:2)프로세스 1S = 2S = 1S = 0S = 0, P3 대기S = 1, P3 깨움S = 0wait()wait()wait()signal()진입 가능

뮤텍스와 세마포어의 차이는 소유권과 다룰 수 있는 자원 수에서 분명해진다.

구분 뮤텍스 세마포어
소유권 있음 (락 획득자만 해제) 없음 (누구나 signal 가능)
바이너리 (0/1) 정수 (0~N)
용도 상호 배제 상호 배제 + 동기화
자원 수 단일 자원 다중 자원
대기 방식 블로킹 블로킹 또는 Busy Waiting

생산자-소비자 문제에서는 빈 슬롯, 채워진 슬롯, 버퍼 접근을 각각 별도의 세마포어로 관리할 수 있다.

세마포어 초기값 용도
empty N 빈 버퍼 슬롯 수
full 0 채워진 버퍼 슬롯 수
mutex 1 버퍼 접근 상호 배제

모니터는 데이터와 동기화를 하나로 묶는다

모니터(Monitor)는 공유 데이터와 그 데이터에 적용하는 연산을 하나의 추상화된 데이터 타입으로 캡슐화하는 고수준 동기화 구조다.

모니터프로시저조건 변수condition xcondition y공유 데이터변수 1변수 2메서드 1메서드 2메서드 3프로세스 1프로세스 2프로세스 3

모니터 안에서는 한 번에 하나의 프로세스만 실행된다. 데이터와 연산을 결합하고 조건 변수를 제공하므로 동기화 코드에서 발생할 수 있는 프로그래머 오류를 줄이는 데 도움이 된다.

특성 설명
상호 배제 자동 보장 한 번에 하나의 프로세스만 모니터 내 실행
캡슐화 데이터와 연산의 결합
조건 변수 특정 조건 대기/신호 메커니즘
고수준 추상화 프로그래머 오류 감소

조건 변수는 특정 조건이 만족될 때까지 대기하고, 조건이 충족되면 기다리던 프로세스를 깨우는 메커니즘이다.

연산 설명
wait(c) 조건 c가 만족될 때까지 대기, 모니터 락 해제
signal(c) 조건 c를 기다리는 프로세스 하나 깨움
broadcast(c) 조건 c를 기다리는 모든 프로세스 깨움

signal() 이후 어느 프로세스가 실행을 이어가는지는 시맨틱스에 따라 달라진다.

signal() 호출시맨틱스?Signal and Wait신호 대기Signal and Continue신호 계속 (Mesa)Signal and Urgent Wait신호 긴급 대기 (Hoare)
시맨틱스 설명 구현
Hoare signal 후 즉시 깨어난 프로세스 실행 드묾
Mesa signal 후 현재 프로세스 계속 실행 Java, POSIX
Brinch Hansen signal은 프로시저 마지막에만 호출 제한적

Java의 synchronized, wait, notify는 모니터 방식의 활용 예다.

class BoundedBuffer {
    private int[] buffer;
    private int count = 0;

    public synchronized void produce(int item) {
        while (count == buffer.length)
            wait();  // 버퍼가 가득 찼으면 대기
        buffer[count++] = item;
        notifyAll();  // 소비자 깨우기
    }

    public synchronized int consume() {
        while (count == 0)
            wait();  // 버퍼가 비었으면 대기
        int item = buffer[--count];
        notifyAll();  // 생산자 깨우기
        return item;
    }
}

Spinlock은 기다리는 동안 CPU를 사용한다

Spinlock은 락을 획득할 때까지 락 상태를 반복 확인하는 방식이다. 블로킹 대신 바쁜 대기(Busy Waiting)를 사용한다.

NoYes 요청 가용?스핀 (반복 확인) 획득임계 영역 실행 해제

Test-and-Set을 사용한 기본 구현은 다음과 같다.

void spin_lock(int *lock) {
    while (test_and_set(lock) == 1)
        ; // 스핀
}

void spin_unlock(int *lock) {
    *lock = 0;
}

짧은 임계 영역과 멀티코어 환경에서는 컨텍스트 스위칭 없이 대기할 수 있다. 긴 임계 영역이나 단일 코어 환경에는 적합하지 않다.

특성 설명
대기 방식 바쁜 대기 (CPU 사용)
컨텍스트 스위칭 없음
적합한 상황 짧은 임계 영역, 멀티코어
부적합한 상황 긴 임계 영역, 단일 코어
구분 Spinlock 뮤텍스
대기 방식 Busy Waiting Blocking (Sleep)
CPU 사용 대기 중 100% 대기 중 0%
컨텍스트 스위칭 없음 있음
적합 환경 짧은 대기, 멀티코어 긴 대기, 모든 환경
커널 사용 가능 (인터럽트 컨텍스트) 불가 (슬립 불가)

Spinlock의 공정성이나 캐시·버스 경합을 다루기 위해 Ticket Lock, MCS Lock, Backoff 기법을 사용할 수 있다.

기법 설명 효과
Ticket Lock FIFO 순서 보장 공정성 향상
MCS Lock 캐시 라인별 스핀 캐시 경합 감소
Backoff 재시도 간 지연 버스 경합 감소

읽기 비중이 큰 작업에는 Read-Write Lock을 검토한다

Read-Write Lock은 읽기와 쓰기를 구분한다. 읽기는 여러 개를 동시에 허용하지만, 쓰기는 배타적으로 처리한다.

Read-Write Lock 상태read_lock추가 read_lock모든 read_unlockwrite_lockwrite_unlock대기 상태읽기 모드(다중 읽기 허용)쓰기 모드(단일 쓰기만)
상황 읽기 요청 쓰기 요청
락 없음 허용 허용
읽기 중 허용 대기
쓰기 중 대기 대기

정책은 읽기 병렬성과 특정 요청의 기아 가능성 사이의 선택이다.

정책 설명 장단점
읽기 우선 읽기 요청 우선 처리 쓰기 기아 가능
쓰기 우선 쓰기 요청 우선 처리 읽기 지연 가능
공정 FIFO 순서 처리 읽기 병렬성 저하

데이터베이스의 읽기 트랜잭션, 캐시 조회, 설정 파일, 라우팅 테이블처럼 조회가 잦고 갱신이 간헐적인 대상에서 활용할 수 있다.

사례 설명
데이터베이스 읽기 트랜잭션 다수, 쓰기 트랜잭션 소수
캐시 시스템 빈번한 조회, 간헐적 갱신
설정 파일 자주 읽고 드물게 수정
라우팅 테이블 패킷 포워딩 시 조회, 경로 변경 시 갱신

재귀적 락과 조건부 락의 쓰임

재귀적 락(Recursive Lock)은 동일 스레드가 여러 번 락을 획득할 수 있게 한다. 획득한 횟수만큼 해제해야 하며, 재귀 함수에서 락을 사용할 때 쓸 수 있다.

특성 설명
중복 획득 동일 스레드의 여러 번 락 획득 허용
해제 횟수 획득 횟수만큼 해제 필요
용도 재귀 함수에서 락 사용

조건 락(Conditional Lock)은 조건을 만족할 때만 락을 얻는다. try_lock은 획득에 실패하면 즉시 반환하고, timed_lock은 지정 시간까지만 대기한다.

특성 설명
조건부 획득 조건 충족 시에만 락 획득
try_lock 락 획득 실패 시 즉시 반환
timed_lock 지정 시간까지만 대기
종류단일 자원, 블로킹짧은 대기, 스핀읽기/쓰기 분리중복 획득 허용MutexSpinlockRW LockRecursive Lock일반적 상호 배제커널, 짧은 임계 영역읽기 위주 워크로드재귀 함수

워크로드에 맞춰 동기화 수단을 고른다

상황 권장 기법 이유
단순 상호 배제 뮤텍스 구현 단순, 안전
다중 자원 관리 세마포어 카운팅 지원
복잡한 동기화 모니터 캡슐화, 조건 변수
짧은 임계 영역 Spinlock 오버헤드 최소화
읽기 위주 RW Lock 읽기 병렬성
고수준 언어 모니터/synchronized 언어 지원

뮤텍스는 상호 배제, 세마포어는 다중 자원 관리, 모니터는 고수준 동기화를 제공한다. Spinlock은 짧은 임계 영역에서 컨텍스트 스위칭 오버헤드를 피하고, Read-Write Lock은 읽기 위주 워크로드의 병렬성을 높인다.

동시성 제어뮤텍스세마포어모니터