리더-라이터 문제 — 동기화 문제의 고전적 해법
데이터베이스, 파일 시스템, 캐시 — 여러 스레드가 동시에 읽고 쓰는 상황은 어디에나 있습니다. 리더-라이터 문제는 이런 상황의 동기화를 다루는 고전적인 문제입니다.
리더-라이터 문제란
여러 Reader와 Writer가 하나의 공유 자원에 접근 할 때의 동기화 문제입니다.
규칙:
- Reader끼리는 동시 접근 가능 — 읽기만 하므로 충돌 없음
- Writer는 배타적 접근 — 쓰는 동안 다른 Reader/Writer 접근 불가
- Writer가 쓰는 동안 Reader도 접근 불가
허용되는 조합:
Reader + Reader → O (동시 읽기 가능)
Reader + Writer → X (충돌)
Writer + Writer → X (충돌)
일반 뮤텍스를 쓰면 Reader끼리도 대기해야 하므로 비효율적입니다. 리더-라이터 문제는 읽기 동시성을 보장하면서 쓰기 안전성을 유지 하는 것이 핵심입니다.
해법 1: Reader 우선 (First Readers-Writers Problem)
Reader가 하나라도 있으면 Writer는 대기합니다.
// 공유 변수
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는 진입하지 못합니다.
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
#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
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)를 지원하여 기아를 방지할 수 있습니다.
// 공정 모드: 가장 오래 기다린 스레드가 우선
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) 지원
정리
| 해법 | 동시 읽기 | 기아 가능성 | 구현 복잡도 |
|---|---|---|---|
| 일반 Mutex | X | 없음 | 쉬움 |
| Reader 우선 | O | Writer 기아 | 중간 |
| Writer 우선 | O | Reader 기아 | 중간 |
| 공정 rwlock | O | 없음 | 높음 |
리더-라이터 문제는 "읽기와 쓰기의 비대칭성"을 활용하는 동기화 패턴입니다. 실제로 캐시나 설정값처럼 읽기가 압도적인 상황에서 rwlock은 큰 성능 이점을 줍니다.