운영체제 락 개념과 동기화 기법 (2. 기본 락(Lock) / 2-3. 읽기-쓰기 락(Read-Write Lock, RWLock))
2025. 2. 26. 12:24ㆍ정보기술/운영체제 (OS)
📖 2-3. 읽기-쓰기 락(Read-Write Lock, RWLock)
읽기-쓰기 락(Read-Write Lock, RWLock)은 읽기 작업은 여러 개의 스레드가 동시에 수행할 수 있지만, 쓰기 작업은 오직 하나의 스레드만 수행할 수 있도록 제한하는 동기화 기법입니다.
멀티스레딩 환경에서 읽기 작업이 많고, 쓰기 작업이 상대적으로 적은 경우 성능을 향상시키는 데 유용합니다.
✅ 읽기-쓰기 락이란?
🔹 기본 개념
- 읽기(Read) 락: 여러 개의 스레드가 동시에 공유 자원을 읽을 수 있음.
- 쓰기(Write) 락: 하나의 스레드만 공유 자원을 수정할 수 있으며, 쓰기 중에는 어떤 스레드도 접근할 수 없음.
- pthread_rwlock_t 를 사용하여 구현.
🔹 읽기-쓰기 락의 필요성
- 일반 Mutex를 사용하면 쓰기뿐만 아니라 읽기 작업도 단일 스레드만 수행해야 하므로 성능이 저하될 수 있음.
- RWLock을 사용하면 여러 개의 스레드가 동시에 읽을 수 있도록 하여 동시성(Concurrency) 향상.
✅ 일반 Mutex와 RWLock 비교
동기화 방식 | 읽기(Read) 접근 | 쓰기(Write) 접근 | 사용 예시 |
Mutex (PTHREAD_MUTEX_NORMAL) | ❌ 하나의 스레드만 접근 가능 | ❌ 하나의 스레드만 접근 가능 | 데이터 변경이 자주 발생하는 경우 |
읽기-쓰기 락 (PTHREAD_RWLOCK) | ✅ 여러 개의 스레드가 동시에 읽기 가능 | ❌ 하나의 스레드만 접근 가능 | 읽기 작업이 많은 경우 |
✅ 동기화가 없는 경우 (문제 발생)
동기화 없이 여러 개의 스레드가 동시에 읽고 쓰기를 수행하면 데이터 충돌(Race Condition) 이 발생할 수 있습니다.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int shared_data = 0; // 공유 자원
void* reader(void* arg) {
printf("Reader %ld: 읽기 작업 수행, 값: %d\n", (long)arg, shared_data);
return NULL;
}
void* writer(void* arg) {
shared_data++;
printf("Writer %ld: 쓰기 작업 수행, 새로운 값: %d\n", (long)arg, shared_data);
return NULL;
}
int main() {
pthread_t r1, r2, w1;
pthread_create(&r1, NULL, reader, (void*)1);
pthread_create(&w1, NULL, writer, (void*)1);
pthread_create(&r2, NULL, reader, (void*)2);
pthread_join(r1, NULL);
pthread_join(w1, NULL);
pthread_join(r2, NULL);
return 0;
}
⚠ 실행 결과 (예측 불가한 결과 발생)
Reader 1: 읽기 작업 수행, 값: 0
Writer 1: 쓰기 작업 수행, 새로운 값: 1
Reader 2: 읽기 작업 수행, 값: 0 <-- ❌ 수정된 데이터가 반영되지 않음!
- 문제점:
- Reader 2가 Writer 1의 수정된 데이터를 반영하지 못함.
- 여러 개의 스레드가 동시에 접근하면서 데이터 정합성(Data Consistency) 문제가 발생할 가능성이 높음.
- 해결 방법: RWLock을 사용하여 동기화.
✅ C 코드 예제 (RWLock 적용)
읽기-쓰기 락을 적용하여 읽기 작업은 여러 개의 스레드가 동시에 수행할 수 있도록 하고, 쓰기 작업은 오직 하나의 스레드만 수행하도록 설정합니다.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int shared_data = 0; // 공유 자원
pthread_rwlock_t rwlock; // 읽기-쓰기 락 선언
void* reader(void* arg) {
pthread_rwlock_rdlock(&rwlock); // 🔒 읽기 락 획득
printf("Reader %ld: 읽기 작업 수행, 값: %d\n", (long)arg, shared_data);
pthread_rwlock_unlock(&rwlock); // 🔓 락 해제
return NULL;
}
void* writer(void* arg) {
pthread_rwlock_wrlock(&rwlock); // 🔒 쓰기 락 획득
shared_data++;
printf("Writer %ld: 쓰기 작업 수행, 새로운 값: %d\n", (long)arg, shared_data);
pthread_rwlock_unlock(&rwlock); // 🔓 락 해제
return NULL;
}
int main() {
pthread_t r1, r2, w1;
pthread_rwlock_init(&rwlock, NULL); // RWLock 초기화
pthread_create(&r1, NULL, reader, (void*)1);
pthread_create(&w1, NULL, writer, (void*)1);
pthread_create(&r2, NULL, reader, (void*)2);
pthread_join(r1, NULL);
pthread_join(w1, NULL);
pthread_join(r2, NULL);
pthread_rwlock_destroy(&rwlock); // RWLock 제거
return 0;
}
✅ 실행 결과 (올바른 동기화)
Reader 1: 읽기 작업 수행, 값: 0
Writer 1: 쓰기 작업 수행, 새로운 값: 1
Reader 2: 읽기 작업 수행, 값: 1 ✅ (수정된 값이 반영됨)
- 읽기 작업은 여러 개의 스레드가 동시에 수행 가능.
- 쓰기 작업은 한 번에 하나의 스레드만 수행 가능.
- 쓰기 작업이 완료된 후, 모든 읽기 작업이 최신 데이터를 참조함.
✅ RWLock 사용 시 주의할 점
🔴 주의해야 할 문제
- 읽기 우선(RWLock이 너무 많은 읽기 작업을 허용하는 경우)
- 읽기 스레드가 너무 많으면 쓰기 스레드가 기다려야 하는 문제 발생.
- 해결책: 쓰기 우선 정책 적용 (pthread_rwlock_wrlock()을 먼저 수행).
- 우선순위 역전 (Priority Inversion)
- 낮은 우선순위의 쓰기 스레드가 계속 대기하는 문제가 발생할 수 있음.
- 해결책: 쓰기 스레드의 우선순위를 높이거나, Fair RWLock 구현.
- 데드락 (Deadlock)
- 여러 개의 스레드가 동시에 읽기-쓰기 락을 서로 기다리는 경우 데드락 발생 가능.
- 해결책: 락 획득 순서 조정.
✅ RWLock vs 일반 Mutex 비교
특징 | 일반 Mutex (PTHREAD_MUTEX_NORMAL) | RWLock (PTHREAD_RWLOCK) |
읽기 작업 동시 수행 가능 여부 | ❌ 불가능 (하나의 스레드만 접근 가능) | ✅ 가능 (여러 개의 스레드가 읽기 가능) |
쓰기 작업 동시 수행 가능 여부 | ❌ 불가능 | ❌ 불가능 (하나의 스레드만 가능) |
성능 | 쓰기와 읽기 모두 병목 발생 가능 | 읽기 작업이 많은 경우 성능 향상 |
사용 예시 | 읽기와 쓰기 작업이 비슷한 비율로 수행되는 경우 | 읽기 작업이 많은 경우 (예: 데이터베이스 캐시) |
✅ 정리
- 읽기-쓰기 락(RWLock)은 읽기 작업은 여러 개의 스레드가 동시에 수행할 수 있도록 하고, 쓰기 작업은 오직 하나의 스레드만 수행하도록 제한하는 락.
- pthread_rwlock_t를 사용하여 구현하며, pthread_rwlock_rdlock()과 pthread_rwlock_wrlock()을 활용하여 동기화.
- 읽기 작업이 많은 경우 RWLock을 사용하면 성능을 향상시킬 수 있음.
- 하지만, 잘못된 사용 시 쓰기 작업이 계속 대기하는 문제(읽기 우선 현상)가 발생할 수 있음.
- 적절한 정책과 우선순위를 설정하여 RWLock을 효과적으로 활용해야 함.