운영체제 락 개념과 동기화 기법 (2. 기본 락(Lock) / 2-1. 뮤텍스(Mutex))

2025. 2. 26. 12:24정보기술/운영체제 (OS)

📖 2-1. 뮤텍스(Mutex)

뮤텍스(Mutex, Mutual Exclusion)는 여러 개의 스레드가 동시에 공유 자원에 접근하는 것을 방지하는 동기화 기법입니다.
운영체제(OS) 내부에서 지원하는 기본적인 동기화 기법으로, 한 번에 하나의 스레드만 자원에 접근할 수 있도록 보장합니다.


✅ 뮤텍스란?

🔹 뮤텍스의 개념

  • Mutex는 상호 배제(Mutual Exclusion) 를 의미하며, 한 번에 오직 하나의 스레드만 공유 자원(임계 영역)에 접근할 수 있도록 보장합니다.
  • 스레드가 Mutex를 획득(lock)하면, 다른 스레드는 Mutex가 해제(unlock)될 때까지 대기 상태에 머뭅니다.
  • pthread_mutex_t를 사용하여 구현되며, 운영체제가 직접 제공하는 동기화 도구 중 하나입니다.

🔹 뮤텍스의 특징

특징  설명
단일 소유권 Lock을 획득한 스레드만 Unlock 가능
Blocking 방식 다른 스레드는 Mutex가 해제될 때까지 대기
경량 동기화 기법 커널 개입이 적어 성능 부담이 적음
데드락 위험 잘못된 사용 시 데드락 발생 가능

✅ 뮤텍스를 사용해야 하는 이유

멀티스레딩 환경에서 여러 개의 스레드가 동시에 공유 자원(Shared Resource) 에 접근하면 경쟁 상태(Race Condition) 가 발생할 수 있습니다.
이를 해결하지 않으면 데이터 손실, 예측 불가능한 오류, 크래시 등의 문제가 발생할 수 있습니다.

🛑 동기화가 없는 경우 (문제 발생)

#include <stdio.h>
#include <pthread.h>

int balance = 1000; // 공유 자원

void* withdraw(void* arg) {
    int temp = balance;  // 현재 잔액 읽기
    temp -= 500;         // 출금 처리
    balance = temp;      // 새로운 값 저장
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, withdraw, NULL);
    pthread_create(&t2, NULL, withdraw, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("최종 잔액: %d\n", balance); // 500이 아닐 수도 있음!
    return 0;
}

⚠ 실행 결과 (경쟁 상태 발생)

최종 잔액: 500
최종 잔액: 1000
최종 잔액: 0  <-- 의도하지 않은 결과 발생 가능!
  • 문제점: 두 개의 스레드가 balance -= 500 연산을 동시에 실행할 경우, 중간 연산 결과가 손실될 가능성이 높음.
  • 해결 방법: 뮤텍스를 사용하여 한 번에 하나의 스레드만 자원을 수정하도록 제한해야 함.

✅ C 코드 예제 (Mutex 적용)

뮤텍스를 사용하여 임계 영역(Critical Section) 을 보호하는 코드입니다.

#include <stdio.h>
#include <pthread.h>

int balance = 1000; // 공유 자원 (임계 영역)
pthread_mutex_t lock; // 뮤텍스 선언

void* withdraw(void* arg) {
    pthread_mutex_lock(&lock);  // 🔒 락 획득 (임계 영역 보호)
    
    int temp = balance;
    temp -= 500;
    balance = temp;

    pthread_mutex_unlock(&lock); // 🔓 락 해제
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&lock, NULL); // 뮤텍스 초기화

    pthread_create(&t1, NULL, withdraw, NULL);
    pthread_create(&t2, NULL, withdraw, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("최종 잔액: %d\n", balance); // 항상 500으로 출력됨

    pthread_mutex_destroy(&lock); // 뮤텍스 제거
    return 0;
}

✅ 실행 결과 (올바른 동기화)

최종 잔액: 500 ✅ (예측 가능한 결과)

🔹 뮤텍스를 적용하면 balance 값을 수정할 때 하나의 스레드만 접근 가능하므로 데이터 무결성이 유지됨.
🔹 pthread_mutex_lock()을 사용하여 보호하고, 작업이 끝나면 반드시 pthread_mutex_unlock()으로 해제해야 함.


✅ 뮤텍스 사용의 장점과 단점

🟢 장점

경쟁 상태(Race Condition) 방지 → 공유 자원의 데이터 무결성을 보장
임계 영역 보호 → 하나의 스레드만 실행 가능
간단한 동기화 기법 → 구현이 비교적 쉬움

🔴 단점

데드락(Deadlock) 가능성 → Lock을 획득한 후 해제하지 않으면 프로그램이 멈춤
병목 현상(Bottleneck) → 락이 걸려있는 동안 다른 스레드는 대기해야 함
우선순위 역전(Priority Inversion) 문제 → 낮은 우선순위 스레드가 락을 오래 잡으면 높은 우선순위 스레드가 지연될 수 있음


✅ 데드락(Deadlock) 문제 해결 방법

🛑 데드락이 발생하는 코드

pthread_mutex_t lock1, lock2;

void* thread1(void* arg) {
    pthread_mutex_lock(&lock1);
    sleep(1); // 일부러 대기
    pthread_mutex_lock(&lock2); // ⚠ 두 번째 락 대기 중 데드락 발생 가능
    printf("Thread 1 실행 중...\n");
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);
    return NULL;
}

void* thread2(void* arg) {
    pthread_mutex_lock(&lock2);
    sleep(1);
    pthread_mutex_lock(&lock1); // ⚠ Thread 1과 교착 상태 발생
    printf("Thread 2 실행 중...\n");
    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);
    return NULL;
}

해결 방법

  1. 락 획득 순서 정하기 → 항상 같은 순서로 Mutex를 획득해야 함.
  2. 타임아웃 적용 → pthread_mutex_timedlock()을 사용하여 일정 시간이 지나면 자동 해제.
  3. Try-Lock 사용 → pthread_mutex_trylock()을 사용하여 실패하면 다른 작업 수행.

✅ 뮤텍스 vs 세마포어 비교

기법  설명  차이점
뮤텍스 (Mutex) 한 번에 하나의 스레드만 접근 가능 Lock을 획득한 스레드만 해제 가능
세마포어 (Semaphore) 여러 개의 스레드가 접근 가능 count 값에 따라 동시 접근 허용 가능

✅ 정리

  1. 뮤텍스(Mutex)상호 배제(Mutual Exclusion) 원칙을 적용하여 하나의 스레드만 임계 영역에 접근할 수 있도록 하는 락.
  2. 경쟁 상태(Race Condition) 문제를 방지하고, 데이터 무결성(Data Integrity)을 보장.
  3. 데드락(Deadlock) 방지를 위해 락 순서를 정하거나 타임아웃을 설정해야 함.
  4. 병렬 처리 성능이 중요한 경우 Semaphore 또는 Spinlock을 고려할 수 있음.