뮤텍스로 Critical Section을 직렬화하는 방법

뮤텍스의 Lock·Unlock 동작과 소유권, Critical Section 직렬화 원리, 공유 카운터·생산자 소비자 문제 적용 방식을 정리한다.

2026-08-14 · 최초 발행 2025-12-25

공유 자원을 한 스레드씩 통과시키는 잠금

뮤텍스(Mutex, Mutual Exclusion)는 Critical Section을 실행하는 스레드들의 시간이 겹치지 않도록, 각 스레드가 단독으로 실행되게 만드는 직렬화 기법이다. 공유 자원에는 한 번에 하나의 스레드만 접근하도록 제한하며, Lock과 Unlock으로 상호 배제를 구현한다. Lock을 얻지 못한 스레드는 대기하고, 이 과정으로 데이터 일관성을 지킨다.

뮤텍스는 0 또는 1의 상태를 가지는 Binary 동기화 기법이며, Lock을 가진 스레드라는 소유권 개념을 둔다.

여러 스레드MutexLock 획득 번에하나만 실행Critical SectionUnlock다음 스레드실행

Lock을 얻은 스레드만 Critical Section에 들어간다

스레드가 Lock을 얻으면 Critical Section을 수행할 수 있다. 이미 Lock이 잡혀 있으면 뒤따르는 스레드는 대기하며, 소유자가 Unlock한 뒤에야 다음 스레드가 Lock을 획득한다.

"스레드 3""스레드 2""Mutex""스레드 1""스레드 3""스레드 2""Mutex""스레드 1""Lock 시도""Lock 획득 (성공)""Critical Section 실행""Lock 시도""대기 (이미 Lock됨)""Lock 시도""대기 (이미 Lock됨)""Unlock""Lock 획득 (T2 차례)""Critical Section 실행"

뮤텍스의 상태는 초기 Unlocked 상태에서 lock() 호출로 Locked가 되고, unlock() 호출로 다시 Unlocked가 된다. 다른 스레드가 Locked 상태에서 Lock을 시도하면 Blocked 상태로 대기한다.

"초기 상태""lock() 호출""unlock() 호출""다른 스레드 lock() 시도""대기 (Queue)""소유자 unlock() 시"UnlockedLockedBlocked

C 환경에서는 초기화, Lock 획득, 해제, 정리의 순서로 사용할 수 있다.

pthread_mutex_t mutex;

// 초기화
pthread_mutex_init(&mutex, NULL);

// 사용
pthread_mutex_lock(&mutex);    // Lock 획득
// Critical Section
pthread_mutex_unlock(&mutex);  // Lock 해제

// 정리
pthread_mutex_destroy(&mutex);

동시 접근을 순차 실행으로 바꾸는 이유

직렬화는 여러 스레드의 Critical Section 실행을 시간적으로 분리해 순차적으로 처리하는 방식이다. 뮤텍스가 없다면 Critical Section이 겹쳐 Race Condition이 생길 수 있고, 뮤텍스를 적용하면 접근 순서가 분리되어 데이터 일관성을 확보한다.

Mutex 적용병렬 실행(동시 접근)Race Condition직렬 실행(순차 접근)데이터 일관성

뮤텍스 없이 실행하면 각 스레드의 Critical Section이 겹친다.

병렬 실행 (Mutex 없음):

스레드 1: ===== [CS] =====
스레드 2:   ===== [CS] =====
스레드 3:     ===== [CS] =====
           ↑ 동시 접근 → Race Condition

뮤텍스를 사용하면 Critical Section은 차례대로 실행된다.

직렬 실행 (Mutex 사용):

스레드 1: ===== [CS] =====
스레드 2:                ===== [CS] =====
스레드 3:                               ===== [CS] =====
           ↑ 순차 접근 → 일관성 보장

공유 카운터를 증가시키는 코드는 읽기·증가·쓰기 사이에 다른 스레드가 끼어들 수 있다.

Mutex 없음 (문제 발생):

int counter = 0;

void* increment(void* arg) {
    int temp = counter;  // 읽기
    temp = temp + 1;     // 증가
    counter = temp;      // 쓰기
    // Race Condition 가능!
}

다음과 같은 순서라면 두 스레드가 모두 0을 읽고 각각 1을 쓰게 된다.

  1. 스레드 1: temp = counter (0)
  2. 스레드 2: temp = counter (0)
  3. 스레드 1: counter = temp + 1 (1)
  4. 스레드 2: counter = temp + 1 (1)
  5. 결과: counter = 1 (2여야 함!)

읽기와 쓰기를 Lock과 Unlock 사이에 두면 해당 구간은 단독으로 실행된다.

Mutex 사용 (문제 해결):

int counter = 0;
pthread_mutex_t lock;

void* increment(void* arg) {
    pthread_mutex_lock(&lock);
    int temp = counter;
    temp = temp + 1;
    counter = temp;
    pthread_mutex_unlock(&lock);
    // 항상 올바른 결과!
}

대기와 소유권이 만드는 실행 규칙

Critical Section에는 한 번에 하나의 스레드만 들어갈 수 있다. Lock을 보유하지 못한 스레드는 대기하며, 실행 순서는 FIFO 또는 우선순위 기반으로 결정된다.

Lock 시도Lock 시도스레드 ALock 보유Critical Section실행스레드 B대기스레드 CUnlock스레드 BLock 획득
스레드 상태 설명 CPU 사용
Running Lock 보유, CS 실행 사용
Blocked Lock 대기 미사용 (Sleep)
Ready Lock 획득 후 실행 대기 미사용

Lock을 획득한 스레드가 소유자이며, Unlock은 소유자만 수행할 수 있다. 재귀 뮤텍스는 같은 스레드가 중복 Lock을 얻을 수 있게 하고, 소유자 추적은 데드락 탐지에 도움이 된다.

// 일반 Mutex (오류)
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);  // 데드락!

// Recursive Mutex (정상)
pthread_mutexattr_t attr;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);

pthread_mutex_lock(&mutex);   // OK
pthread_mutex_lock(&mutex);   // OK (카운터 증가)
pthread_mutex_unlock(&mutex); // 카운터 감소
pthread_mutex_unlock(&mutex); // Lock 해제

공유 상태를 갱신하는 코드에서의 사용

생산자와 소비자가 버퍼와 count를 함께 다룰 때, 뮤텍스는 해당 상태 변경 구간을 보호한다.

int buffer[SIZE];
int count = 0;
pthread_mutex_t mutex;

void* producer(void* arg) {
    while (1) {
        int item = produce_item();
        pthread_mutex_lock(&mutex);
        if (count < SIZE) {
            buffer[count++] = item;
        }
        pthread_mutex_unlock(&mutex);
    }
}

void* consumer(void* arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        if (count > 0) {
            int item = buffer[--count];
            pthread_mutex_unlock(&mutex);
            consume_item(item);
        } else {
            pthread_mutex_unlock(&mutex);
        }
    }
}

계좌 이체처럼 두 공유 객체를 함께 갱신하는 경우에는 Lock 획득 순서를 고정해 데드락을 피할 수 있다.

typedef struct {
    int balance;
    pthread_mutex_t lock;
} Account;

void transfer(Account* from, Account* to, int amount) {
    // 데드락 방지: 항상 같은 순서로 Lock
    Account* first = (from < to) ? from : to;
    Account* second = (from < to) ? to : from;

    pthread_mutex_lock(&first->lock);
    pthread_mutex_lock(&second->lock);

    from->balance -= amount;
    to->balance += amount;

    pthread_mutex_unlock(&second->lock);
    pthread_mutex_unlock(&first->lock);
}

직렬화 비용을 다루는 선택지

뮤텍스는 데이터 일관성과 간단한 구현을 제공하지만, 직렬화에 따른 성능 저하, 데드락 위험, 컨텍스트 스위칭 오버헤드도 수반한다.

Mutex 사용장점단점데이터 일관성간단한 구현성능 저하(직렬화)데드락 위험컨텍스트 스위칭오버헤드

세밀한 Lock을 위해 여러 Mutex를 두면 병렬성을 높일 수 있다. 가능하다면 원자적 연산(Atomic Operations)을 사용하는 Lock-Free 방식도 선택지다. 읽기/쓰기 Lock은 읽기를 동시에 허용하고, 쓰기만 독점한다.

뮤텍스동기화운영체제멀티스레드동시성