본문 바로가기

진리는어디에

[C/C++] Read/Write lock

rw lock

Read/Write Lock의 개념

Read/Write Lock(이하 rwlock)의 기본적인 개념은 아래와 같이 간단하다.

  • Read 작업은 값을 변경하지 않으니, 하나의 크리티컬 섹션에 여러개의 스레드가 진입해도 일관성 관련 문제가 발생하지 않는다.
  • Write 작업은 값을 변경 할 수도 있으니, 하나의 크리티컬 섹션에 여러개의 스레드가 진입 할 경우 일관성이 깨질 수 있다.
  • Read 작업은 여러개의 스레드가 하나의 크리티컬 섹션에 접근 가능하지만, Read하는 도중 값이 변경 되면 안된다.

위의 세 가지 사항 정도만 제외 한다면 rwlock은 mutex랑 비슷하다. 개념도 비슷하고, 사용법도 비슷하고, 하는 일도 비슷하다.

rwlock의 장점은 read 작업에 있어서 여러개의 쓰레드가 서로 블록킹 하는 일이 없어 괜한 오버헤드를 발생 시키지 않고, 단점은 그 특성상 read작업이 많다면 거기에 눌려 write 오퍼레이션은 뒤로 밀리기 마련이다.

Write lock을 걸려고 하는 쓰레드가 자원을 못 받아 말라 죽는 시나리오를 만들어 보자.

  1. 1번 쓰레드가 A라는 공유 자원을 참조 하려고 한다.
    아직 아무런 lock이 걸려 있지 않으므로 커널은 lock을 허락한다.
  2. 2번 쓰레드가 A라는 공유 자원을 변경 하려고 한다.
    변경이라는 것은 wrtie lock을 걸어야 하는데 아직 read 작업이 있으므로 write를 잠시 멈춘다.
    하지만 그 사이에 다른 read 작업이 또 들어 온다. read 작업은 동시에 여러개가 진행 될 수 있으므로 커널은 뒤늦게 요청된 read 작업을 크리티컬섹션 안으로 넣어 버린다. 이런 현상의 반복으로 write를 요청하는 스레드는 굶어 죽는 경우가 발생 할 수 있다.(현실적으로는 이런 경우를 대비하여 만들었 겠지만, 그래도 rwlock은 modify작업 보다는 read가 더 많은 곳에 사용하는 것이 가장  이상적이다)

NOTE - 쓰레드 기아방지 관련 업데이트 되었음

API

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

API가 상당히 직관적이다. mutex같은 것들과 마찬가지로 pthread_rwlock_t 객체를 사용하기 이전에 pthread_rwlock_init으로 객체를 초기화 해주고, 사용이 끝나고 커널에 반환 해야 할 경우에는 pthread_rwlock_destroy로 객체를 해제해 준다.

Read 작업을 하는 경우에는 pthread_rwlock_rdlock을 호출 하고, write작업을 할경우에는 pthread_rwlock_wrlock을 호출 하면 된다. 그리고 어떠한 read 작업이든, write작업이든 lock을 풀어 줄때는 pthread_rwlock_unlock을 호출 한다.

read작업을 다른 말로 shared 작업, 즉 공유가 가능한 작업이라고 하는데 구현상 그 갯수에 제한을 두고 있다.

Single Unix 계열에서는 아래와 같은 함수를 지원한다.

#include <pthread.h>

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

try라는 이름과 같이 단시 시도를 할 뿐이다. 만일 lock을 잡을 수 없는 상황이라면 바로 리턴을 한다. 리턴 값은 lock를 잡을 경우에는 0을 리턴하고, 그렇지 못하다면 EBUSY를 리턴한다.

Example

아래의 예제 코드는 두개의 read lock과 하나의 write lock을 사용한다. 하나의 read lock을 걸고 다른 read lock을 걸었을 경우 과연 예상대로 lock이 잡히는지 안잡히는지, 그리고 read lock가 걸려 있는 상태에서 write lock는 과연 대기 상태에 있는지를 알아 보고자 하는 코드다.

#include <pthread.h>
#include <iostream>
#include <unistd.h>

pthread_rwlock_t g_rwLock;
int g_share = 0;

void* thr_readLock(void* arg) {
    int ret = pthread_rwlock_rdlock(&g_rwLock);
    if(ret == 0) {
        std::cout << "try to read lock successfully complete!!" << std::endl;
    }
    
    sleep(3);
    
    pthread_rwlock_unlock(&g_rwLock);
    
    std::cout << "unlock the read lock" << std::endl;
}

void* thr_writeLock(void* arg) {
    int ret = pthread_rwlock_wrlock(&g_rwLock);
    if(ret == 0) {
        std::cout << "try to write lock successfully complete!!" << std::endl;
    }
    
    pthread_rwlock_unlock(&g_rwLock);
    
    std::cout << "unlock the write lock" << std::endl;
}

int main() {
    int ret = 0;
    ret = pthread_rwlock_init(&g_rwLock, NULL);
    if(ret != 0) {
        std::cout << strerror(ret) << std::endl;
        exit(1);
    }
    
    pthread_t readThr[2];
    pthread_t writeThr;
    
    ret = pthread_create(&readThr[0], NULL, thr_readLock, NULL);
    if(ret != 0) {
        std::cout << strerror(ret) << std::endl;
        exit(1);
    }
    
    ret = pthread_create(&readThr[1], NULL, thr_readLock, NULL);
    if(ret != 0) {
        std::cout << strerror(ret) << std::endl;
        exit(1);
    }
    
    ret = pthread_create(&writeThr, NULL, thr_writeLock, NULL);
    if(ret != 0) {
        std::cout << strerror(ret) << std::endl;
        exit(1);
    }
 
    sleep(10);
    pthread_rwlock_destroy(&g_rwLock);
}

부록 1. 같이 읽으면 좋은 글

부록 2. Write 스레드 기아 방지

위에서 write 하려는 쓰레드가 lock을 잡지 못해 굶어 죽을 수 있다고 했지만, 그것은 이론상 그런 시나리오가 발생 할 수 있다는 이야기고, https://opensource.apple.com/source/Libc/Libc-498.1.7/pthreads/pthread_rwlock.c 의 실제 구현되어 있는 코드를 살펴보면 아래 처럼 write lock을 잡기 위해 rwlock_wrlock을 호출 할 때 blocked_writers를 증가 시켜, read lock을 잡기 위해 rwlock_rdlock을 호출 할 때 이전에 들어온 write lock 요청을 검사한다. write lock 요청이 있는 경우 read lock 잡는 것을 블록 시킴으로써 쓰레드가 굶어 죽는 것을 방지 한다.

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) {
    // ... 생략 ...
    ++rwlock->blocked_writers;
    // ... 생략 ...
}

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) {
    // ... 생략 ...
#if __DARWIN_UNIX03
	while (rwlock->blocked_writers || ((rwlock->state < 0) && (rwlock->owner != self))) 
#else /* __DARWIN_UNIX03 */
	while (rwlock->blocked_writers || rwlock->state < 0) 
#endif /* __DARWIN_UNIX03 */
    {
    }
    // ... 생략 ...
}
유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!