본문 바로가기
카테고리 없음

OS - 동기화 세마포어 뮤텍스

by kik328288 2026. 5. 18.

동기화 (세마포어, 뮤텍스, 데드락)

여러 스레드가 같은 메모리를 공유한다는 멀티스레드의 강점은 그대로 동기화 문제의 출발점이 된다. 두 스레드가 같은 변수에 동시에 쓰면 결과가 실행 순서에 따라 달라지는 경쟁 조건(race condition)이 발생하고, 그 결과는 매번 다른 값으로 떨어진다. 이 문제를 푸는 도구가 동기화 메커니즘이며, 그 중에서도 가장 자주 등장하는 세 가지가 뮤텍스(Mutex)·세마포어(Semaphore)·모니터(Monitor)이다. 본 글은 임계 구역의 정의부터 세 동기화 도구의 차이, 그리고 잘못 쓰면 발생하는 데드락(Deadlock)까지 세심하게 정리한다(출처: 위키백과 — Mutual exclusion). 제가 학교 OS 수업에서 처음 같은 카운터를 두 스레드로 1만 번씩 증가시켜 봤을 때 결과가 매번 다르게 나오는 모습이 가장 큰 충격이었고, mutex 한 줄을 거는 것만으로 결과가 항상 2만이 되는 변화를 직접 본 후로는 동기화가 단순한 학문적 개념이 아니라 코드의 정확성을 결정하는 핵심 도구라는 점을 손끝으로 받아들였다.

 

임계 구역과 경쟁 조건 — 동기화가 필요한 이유

임계 구역(Critical Section)이란 여러 프로세스 또는 스레드가 동시에 접근해서는 안 되는 공유 자원을 다루는 코드 영역을 의미한다. 한 번에 한 스레드만 들어갈 수 있어야 데이터의 일관성이 보장되며, 임계 구역의 진입·실행·퇴출이 원자적으로 일어나야 한다. 여기서 원자성(atomicity)이란 한 작업이 다른 작업의 개입 없이 통째로 수행되거나 통째로 수행되지 않는 성질을 가리키며, 동기화 메커니즘이 보장해야 할 가장 본질적인 속성이다.

// 동기화 없는 카운터 — race condition 발생
int counter = 0;

void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        counter++;       // 1) load 2) +1 3) store — 세 단계 모두 인터럽트 가능
    }
    return NULL;
}
// 두 스레드 실행 시 counter는 200000이 아니라 매번 다른 값

counter++는 한 줄짜리 C 코드처럼 보이지만 어셈블리 수준에서는 load → +1 → store 세 단계로 분해되며, 그 중 어느 지점에서든 다른 스레드가 끼어들 수 있다. 두 스레드가 거의 같은 시점에 load를 했다면 같은 값을 읽어 와 각자 +1을 한 뒤 같은 값을 store하기 때문에, 결국 +1이 두 번 일어나야 할 자리에서 +1이 한 번만 반영되는 사고가 발생한다.

임계 구역 문제를 해결하기 위한 동기화 메커니즘이 만족해야 할 세 가지 조건이 있다. 시험에서 자주 출제되는 항목이다. 첫째 상호 배제(Mutual Exclusion) — 한 번에 한 스레드만 임계 구역에 진입할 수 있어야 한다. 둘째 진행(Progress) — 임계 구역에 들어가지 못한 스레드는 결국 들어갈 수 있어야 한다(무한 대기 금지). 셋째 한정 대기(Bounded Waiting) — 한 스레드의 진입 요청이 무한정 미뤄지지 않아야 한다. 이 세 조건을 동시에 만족시키는 것이 동기화 알고리즘의 정확성 기준이다.

뮤텍스·세마포어·모니터 — 세 동기화 도구

가장 단순한 동기화 도구가 뮤텍스(Mutex, Mutual Exclusion)이다. 뮤텍스는 단 두 상태(잠금·해제)만 가지는 이진 락(binary lock)이며, 한 번에 한 스레드만 잠글 수 있다. 잠금을 얻은 스레드가 해제하기 전까지 다른 스레드는 모두 대기한다. POSIX 환경의 pthread_mutex_t, C++의 std::mutex, 자바의 synchronized 키워드가 모두 같은 부류이다.

#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

void* safe_increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&lock);      // 임계 구역 진입
        counter++;
        pthread_mutex_unlock(&lock);    // 임계 구역 퇴출
    }
    return NULL;
}
// 두 스레드 실행 시에도 counter는 항상 200000

세마포어(Semaphore)는 1965년 다익스트라가 제안한 더 일반화된 동기화 도구이다(출처: 위키백과 — Semaphore). 정수 값을 유지하면서 P(wait, 감소)와 V(signal, 증가) 두 연산만으로 제어되며, 정수 값이 0보다 크면 즉시 통과하고 0 이하면 대기 큐에 들어간다. 여기서 P와 V는 다익스트라가 네덜란드어로 명명한 약어이며 각각 "passeren(통과)"와 "vrijgeven(해제)"의 첫 글자에서 유래했다.

구분 뮤텍스(Mutex) 카운팅 세마포어(Counting Semaphore) 이진 세마포어(Binary Semaphore)
값의 범위 잠금/해제 (2 상태) 0 이상 정수 0 또는 1
의미 한 자원에 한 스레드만 N개 자원에 N개 스레드까지 시그널·이벤트 알림
소유 개념 있음 (잠근 스레드만 해제) 없음 없음
대표 사용처 임계 구역 보호 자원 풀(DB 커넥션 풀) 생산자-소비자 알림

뮤텍스와 이진 세마포어는 겉모습이 거의 같지만, 결정적인 차이가 소유권(ownership)의 유무에 있다. 뮤텍스는 잠근 스레드만 풀 수 있지만, 이진 세마포어는 누가 잠갔든 누구나 풀 수 있다. 이 차이 때문에 임계 구역 보호에는 뮤텍스가, 한 스레드가 다른 스레드에게 신호를 보내는 알림 메커니즘에는 세마포어가 적합하다.

모니터(Monitor)는 자바의 synchronized와 파이썬의 threading.Condition이 채택한 고수준 동기화 추상화로, 임계 구역 진입과 조건 대기를 함께 묶어 제공한다. 모니터 안의 모든 메서드는 자동으로 상호 배제가 보장되고, wait·notify·notifyAll 같은 조건 변수로 정교한 흐름을 표현할 수 있다.

# Python — Lock(뮤텍스)으로 카운터 보호
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100_000):
        with lock:               # __enter__ → acquire, __exit__ → release
            counter += 1

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter)   # 항상 200000

데드락(Deadlock)과 회피·예방 전략

동기화 도구를 잘못 쓰면 새로운 사고인 데드락(Deadlock, 교착 상태)이 발생한다. 데드락이란 둘 이상의 프로세스(또는 스레드)가 서로의 자원을 기다리며 영원히 진행하지 못하는 상태를 가리킨다. 시험에서 매회 출제되는 핵심 영역이며, 데드락 발생 조건 네 가지를 정확히 외워두어야 한다(출처: Coffman 조건 — Wikipedia).

조건 의미
상호 배제(Mutual Exclusion) 자원이 한 번에 한 프로세스에만 할당
점유와 대기(Hold and Wait) 자원을 가진 채 다른 자원을 추가로 요청
비선점(No Preemption) 자원을 강제로 빼앗을 수 없음
순환 대기(Circular Wait) 자원 요청이 원형 사슬을 형성

이 네 조건이 동시에 모두 만족되어야 데드락이 발생한다. 따라서 어느 한 조건이라도 깨면 데드락을 막을 수 있으며, 이것이 데드락 예방(prevention)의 기본 전략이다. 가장 자주 쓰이는 예방 기법이 자원 순서 부여(resource ordering)로, 모든 자원에 전역 번호를 매기고 항상 번호가 낮은 자원부터 잠그는 규칙을 강제하면 순환 대기가 원천적으로 차단된다.

데드락을 다루는 운영체제 차원의 4가지 전략은 다음과 같다.

  1. 예방(Prevention) — 네 조건 중 하나를 사전에 막는다 (자원 순서 부여 등)
  2. 회피(Avoidance) — 자원 할당 시 안전 상태인지 검사 (은행원 알고리즘)
  3. 탐지·복구(Detection & Recovery) — 발생을 허용하고 주기적으로 탐지 후 강제 종료
  4. 무시(Ostrich Algorithm) — 발생이 드물면 무시 (Unix·Windows 커널의 일부 채택)

여기서 은행원 알고리즘(Banker's Algorithm)이란 다익스트라가 제안한 데드락 회피 알고리즘으로, 자원 할당 요청이 들어올 때마다 그 요청을 수락해도 모든 프로세스가 안전하게 종료될 수 있는지를 시뮬레이션해 미리 거부할지 수락할지를 결정한다. 솔직히 제 경험상 학교 OS 시험에서 가장 자주 함정으로 나왔던 게 "데드락이 발생하려면 네 조건이 모두 만족해야 한다"는 한 줄이었고, 이 한 줄을 외운 사람만 객관식·서술형 모두에서 안정적으로 점수를 따 갔다.

마지막으로 시험 답안에서 자주 쓰이는 정형 표현을 정리하면, "동기화는 여러 프로세스·스레드가 공유 자원을 다룰 때 임계 구역을 보호해 데이터의 일관성을 보장하는 메커니즘이다. 대표 도구로 뮤텍스(이진 락·소유권 있음), 세마포어(정수 값·소유권 없음), 모니터(고수준 추상화)가 있으며, 잘못 사용하면 네 가지 조건(상호 배제·점유와 대기·비선점·순환 대기)이 동시에 만족되는 데드락이 발생한다"는 두 문장이 표준 답안 표현으로 통한다. 다음 글에서는 페이지 교체·가상 메모리 같은 OS 메모리 관리 영역을 이어 다룬다.


메타 디스크립션: 동시성 프로그래밍의 핵심인 임계 구역·경쟁 조건과 그 해결책인 뮤텍스·세마포어·모니터의 차이, 그리고 데드락 발생 4조건과 4가지 처리 전략(예방·회피·탐지·무시)을 코드 예시와 함께 OS 입문자 관점에서 세심하게 정리합니다.


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 블로그 이름