[Deep Dive] 쓰레드와 병렬 프로그래밍 - 2탄 동기화 기법 Mutex

1. pthread_mutex_t: 상호배제(Mutual Exclusion)

개념

  • 여러 쓰레드가 공유 자원에 동시 접근하지 못하도록 막는 도구.
  • 한 번에 하나의 쓰레드만 임계구역(Critical Section)을 실행할 수 있도록 보장.

주요 함수

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 정적 초기화

pthread_mutex_init(&lock, NULL);     // 동적 초기화
pthread_mutex_lock(&lock);           // 락 획득 (다른 쓰레드가 잡고 있으면 대기)
pthread_mutex_unlock(&lock);         // 락 해제
pthread_mutex_destroy(&lock);        // 소멸 (동적으로 생성한 경우)

사용 예시

int shared_data = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* thread_func(void* arg) {
    pthread_mutex_lock(&lock);
    shared_data += 1;  // 임계구역
    pthread_mutex_unlock(&lock);
    return NULL;
}

❗주의사항

  • pthread_mutex_lock을 호출한 쓰레드는 반드시 unlock 해야 함.
  • unlock 전에 리턴하거나 종료되면 데드락 발생 가능.

딥 다이브 영역

더보기

Mutex란 무엇인가? :

Mutex는 "Mutual Exclusion" 의 줄입말로, 임계 구역(Critical Section)에서 동시에 둘 이상의 쓰레드가 접근하는 것을 방지하는 동기화 수단이다. 즉 하나의 쓰레드만 해당 코드 영역에 들어갈 수 있도록 문을 잠그고(lock), 작업이 끝나면 문을 여는(unlock) 방식이다.

임계 영역은 주로 공유 자원에 대한 접근 코드이다. 예: 전역 변수, 파일, 소켓, 데이터베이스 등

pthread_mutex_lock(&mutex);
// 공유 자원 접근
pthread_mutex_unlock(&mutex);

이렇게 하면 한 번에 한 쓰레드만 임계 구역을 실행할 수 있다.


임계 구역 (Critical Section)이란?

정의:

임계 구역이란, 여러 쓰레드가 동시에 실행할 경우 문제가 생길 수 있는 "공유 자원"을 접근하는 코드 영역을 말한다.

즉,

  • 공유 자원: 전역 변수, 힙 메모리, 파일, 소켓 등
  • 임계 구역: 위 공유 자원을 읽거나 쓰는 코드 블록

이 구역은 한 번에 오직 하나의 쓰레드만 들어가야 한다. 그렇지 않으면 데이터 손상, 비정상 동작, race condition 등의 문제가 발생한다.

예시로 이해하기

int count = 0;

void* thread_func(void* arg) {
    for (int i = 0; i < 10000; i++) {
        count++;  // 💥 임계 구역
    }
    return NULL;
}

위 코드에서 count++는 단순한 증가처럼 보이지만, 내부적으로는 다음 3단계가 발생한다:

  1. count 읽기 (load)
  2. +1 연산
  3. 결과를 다시 저장 (store)

두 쓰레드가 동시에 count++를 하면 덮어쓰기나 손실이 발생할 수 있다. 예: 0 → 1 → 2가 돼야 하는데, 실제로는 두 번 증가했지만 count == 1일 수 있다.

그래서 뮤텍스(Mutex)가 필요한 이유

pthread_mutex_lock(&lock);
count++;  // 🛡️ 임계 구역 보호
pthread_mutex_unlock(&lock);

이렇게 하면 동시에 하나의 쓰레드만 count를 접근할 수 있게 되어 데이터 손상을 방지할 수 있다.


Race Condition이란?

Race Condition은 "여러 쓰레드가 경쟁적으로 임계 구역을 동시에 실행해서 예상치 못한 결과가 발생하는 현상"이다. 반드시 임계 구역은 동기화 기법으로 보호해야 한다.


2. 주요 함수 설명 (POSIX Threads, 즉 pthread 사용)

2.1 pthread_mutex_t

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 뮤텍스를 선언하고 초기화한다.
  • 매크로로 초기화할 수도 있고, 함수로도 초기화할 수 있다.

2.2 pthread_mutex_init

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 뮤텍스를 초기화하는 함수.
  • attr은 뮤텍스 속성으로 NULL을 주면 기본 속성 사용.
  • 리턴값이 0이면 성공, 에러 시 EINVAL, ENOMEM 등의 에러코드.

2.3 pthread_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 뮤텍스를 획득한다.
  • 이미 다른 쓰레드가 해당 뮤텍스를 보유 중이라면 블록(block) 상태가 된다 (대기).
  • 리턴값 0이면 성공. 다른 값이면 에러.

2.4 pthread_mutex_trylock

int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 뮤텍스를 즉시 획득 시도한다.
  • 다른 쓰레드가 이미 잠금 중이면 실패하고 -EBUSY 리턴 (블록 되지 않음).
  • 주로 non-blocking 하게 동작시키고 싶을 때 사용.

2.5 pthread_mutex_unlock

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 뮤텍스를 해제한다.
  • 해당 뮤텍스를 보유한 쓰레드만 해제할 수 있다.

2.6 pthread_mutex_destroy

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 뮤텍스를 파괴하여 자원을 해제한다.
  • 뮤텍스를 잠근 상태에서는 destroy 하면 안 됨. unlock 이후 destroy 해야 한다.

딥 다이브 영역

더보기

Deadlock (교착상태)

  • A 쓰레드가 mutex1 → mutex2를 lock하고, B 쓰레드는 mutex2 → mutex1을 lock하려는 경우.
  • 뮤텍스 획득 순서를 명확히 설계하거나 trylock으로 회피 가능성 존재.

Starvation (기아 상태)

  • 우선순위 낮은 쓰레드가 계속 mutex를 얻지 못해 실행이 밀리는 경우.

3. 뮤텍스 속성 설정

pthread_mutex 속성 설정

구조체와 초기화

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);

pthread_mutexattr_settype의 옵션들

타입 이름 설명
PTHREAD_MUTEX_NORMAL 기본 타입. 같은 쓰레드가 두 번 락을 걸면 데드락 발생
PTHREAD_MUTEX_ERRORCHECK 같은 쓰레드가 다시 락을 걸면 오류 반환
PTHREAD_MUTEX_RECURSIVE 같은 쓰레드가 여러 번 락을 걸 수 있음 (unlock도 횟수만큼 필요)
PTHREAD_MUTEX_DEFAULT 시스템 기본값. 보통 NORMAL과 같음

재귀적 뮤텍스 (PTHREAD_MUTEX_RECURSIVE)

왜 필요한가? 다음처럼 함수 내에서 재귀적으로 락을 걸 필요가 있는 경우:

pthread_mutex_lock(&mutex);
function(); // 내부에서 다시 pthread_mutex_lock(&mutex)
pthread_mutex_unlock(&mutex);

기본 뮤텍스에서는 위와 같이 동일한 쓰레드가 재진입하면 데드락 발생.
→ PTHREAD_MUTEX_RECURSIVE 사용하면 락 횟수를 기록하고, 그만큼 unlock 해야 해제된다.