데이터베이스, 파일 시스템, 캐시 — 여러 스레드가 동시에 읽고 쓰는 상황은 어디에나 있습니다. 리더-라이터 문제는 이런 상황의 동기화를 다루는 고전적인 문제입니다.


리더-라이터 문제란

여러 Reader와 Writer가 하나의 공유 자원에 접근 할 때의 동기화 문제입니다.

리더-라이터 문제 구조

규칙:

  • Reader끼리는 동시 접근 가능 — 읽기만 하므로 충돌 없음
  • Writer는 배타적 접근 — 쓰는 동안 다른 Reader/Writer 접근 불가
  • Writer가 쓰는 동안 Reader도 접근 불가
PLAINTEXT
허용되는 조합:
Reader + Reader  → O (동시 읽기 가능)
Reader + Writer  → X (충돌)
Writer + Writer  → X (충돌)

일반 뮤텍스를 쓰면 Reader끼리도 대기해야 하므로 비효율적입니다. 리더-라이터 문제는 읽기 동시성을 보장하면서 쓰기 안전성을 유지 하는 것이 핵심입니다.


해법 1: Reader 우선 (First Readers-Writers Problem)

Reader가 하나라도 있으면 Writer는 대기합니다.

C
// 공유 변수
int read_count = 0;        // 현재 읽는 중인 Reader 수
semaphore mutex = 1;        // read_count 보호
semaphore rw_mutex = 1;     // Writer 진입 제어

// Reader
void reader() {
    wait(mutex);            // read_count 수정 보호
    read_count++;
    if (read_count == 1)
        wait(rw_mutex);     // 첫 번째 Reader가 Writer를 차단
    signal(mutex);

    // === 읽기 수행 ===

    wait(mutex);
    read_count--;
    if (read_count == 0)
        signal(rw_mutex);   // 마지막 Reader가 Writer 허용
    signal(mutex);
}

// Writer
void writer() {
    wait(rw_mutex);         // 배타적 접근

    // === 쓰기 수행 ===

    signal(rw_mutex);
}

문제점: Writer 기아(Starvation) — Reader가 계속 들어오면 Writer가 영원히 대기


해법 2: Writer 우선 (Second Readers-Writers Problem)

Writer가 대기 중이면 새로운 Reader는 진입하지 못합니다.

C
int read_count = 0, write_count = 0;
semaphore r_mutex = 1;       // read_count 보호
semaphore w_mutex = 1;       // write_count 보호
semaphore rw_mutex = 1;      // 읽기-쓰기 상호 배제
semaphore queue = 1;         // Reader 진입 제어

// Writer
void writer() {
    wait(w_mutex);
    write_count++;
    if (write_count == 1)
        wait(queue);         // 대기 중인 Writer가 있으면 새 Reader 차단
    signal(w_mutex);

    wait(rw_mutex);
    // === 쓰기 수행 ===
    signal(rw_mutex);

    wait(w_mutex);
    write_count--;
    if (write_count == 0)
        signal(queue);
    signal(w_mutex);
}

// Reader
void reader() {
    wait(queue);             // Writer가 대기 중이면 여기서 블록
    wait(r_mutex);
    read_count++;
    if (read_count == 1)
        wait(rw_mutex);
    signal(r_mutex);
    signal(queue);

    // === 읽기 수행 ===

    wait(r_mutex);
    read_count--;
    if (read_count == 0)
        signal(rw_mutex);
    signal(r_mutex);
}

문제점: Reader 기아 — Writer가 계속 들어오면 Reader가 영원히 대기


해법 3: 공정한 방식

Reader와 Writer 모두 기아 없이 처리하는 방법입니다.

  • 도착 순서대로 처리 (FIFO)
  • 또는 Writer가 들어오면 현재 Reader들이 끝날 때까지만 기다리고, 그 후 Writer 실행

실제 구현: ReadWriteLock

POSIX pthread_rwlock

C
#include <pthread.h>

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

// Reader
pthread_rwlock_rdlock(&rwlock);   // 읽기 잠금 (여러 Reader 동시 가능)
// 읽기 수행
pthread_rwlock_unlock(&rwlock);

// Writer
pthread_rwlock_wrlock(&rwlock);   // 쓰기 잠금 (배타적)
// 쓰기 수행
pthread_rwlock_unlock(&rwlock);

Java ReentrantReadWriteLock

JAVA
ReadWriteLock lock = new ReentrantReadWriteLock();

// Reader
lock.readLock().lock();
try {
    // 읽기 수행
} finally {
    lock.readLock().unlock();
}

// Writer
lock.writeLock().lock();
try {
    // 쓰기 수행
} finally {
    lock.writeLock().unlock();
}

Java의 ReentrantReadWriteLock공정 모드(fair mode)를 지원하여 기아를 방지할 수 있습니다.

JAVA
// 공정 모드: 가장 오래 기다린 스레드가 우선
ReadWriteLock fairLock = new ReentrantReadWriteLock(true);

적용 사례

상황읽기/쓰기 비율적합한 방식
설정값 조회읽기 99%, 쓰기 1%ReadWriteLock
캐시읽기 90%, 쓰기 10%ReadWriteLock 또는 ConcurrentHashMap
로그 파일쓰기 위주일반 Mutex가 나을 수 있음
DB 인덱스읽기 위주B-Tree의 읽기-쓰기 잠금

읽기 비율이 높을수록 ReadWriteLock의 이점이 큽니다. 쓰기 비율이 높으면 rwlock의 오버헤드 때문에 일반 mutex가 나을 수 있습니다.


핵심 포인트

  • 왜 일반 뮤텍스 대신 rwlock을 쓰는가: Reader끼리는 동시 접근 가능 → 읽기 위주 워크로드에서 처리량 향상
  • Reader/Writer 기아 문제: 어느 한쪽 우선이면 다른 쪽이 기아 → 공정 모드 필요
  • Java의 StampedLock: ReadWriteLock보다 성능이 좋은 낙관적 읽기(optimistic read) 지원

정리

해법동시 읽기기아 가능성구현 복잡도
일반 MutexX없음쉬움
Reader 우선OWriter 기아중간
Writer 우선OReader 기아중간
공정 rwlockO없음높음

리더-라이터 문제는 "읽기와 쓰기의 비대칭성"을 활용하는 동기화 패턴입니다. 실제로 캐시나 설정값처럼 읽기가 압도적인 상황에서 rwlock은 큰 성능 이점을 줍니다.

댓글 로딩 중...