RUST

[RUST] RWLock (Read-Write Lock)

ssh9308 2024. 5. 5. 20:18
반응형

RWLock (Read-Write Lock) 이란?

 

RWLock은 데이터에 대한 읽기와 쓰기 접근을 다르게 관리할 수 있는 동기화 메커니즘이다.

 

RWLock을 사용하면, 여러 스레드가 동시에 데이터를 읽을 수 있지만,

 

데이터에 쓰기를 할 때는 독점적인 접근이 필요하다.

 

 

 

RWLock의 작동방식

 

RWLock은 Mutex와 비슷해 보이지만,

 

공유데이터에 대한 쓰기 알고리즘이 다른 방식으로 작동한다.

 

Mutex는 읽기, 쓰기 모두 배타적 잠금(Exclusive Lock)을

 

유지한 상태로 공유데이터에 접근하지만,

 

RWLock 은 Read, Write에 따라 각기 다른 잠금 메커니즘이 존재한다.

 

 

1) 읽기 접근 (Read Lock)

 

여러 스레드가 동시에 공유 데이터를 읽을 수 있다.

 

읽기 작업이 이루어지는 동안에는 새로운 읽기 잠금이 추가적으로 획득될 수 있으며,

 

이는 데이터의 일관성을 유지하는 데 도움이 된다.

 

 

2) 쓰기 접근 (Write Lock)

 

쓰기 작업을 수행하는 스레드는 데이터에 대한 독점적 접근 권한을 요구한다.

 

쓰기 잠금이 활성화되는 동안에는 다른 어떤 스레드도 데이터를 읽거나 쓸 수 없다.

 

즉, 쓰기 작업을 진행할 때는 배타적 잠금(Exclusive Lock)이 활성화된다.

 

 

 

RWLock의 사용 예제

 

RWLock을 실제로 어떤 식으로 사용하는지 알아보자.

 

여기서는 Mutex와의 차이점을 알아보기 위해,

 

RWLock, Mutex 두 개의 알고리즘을 비교해 보자.

 

 

1) Mutex

 

fn mutex_test() {

    let mutex_lock = Arc::new(Mutex::new(0));

    let reader1 = Arc::clone(&mutex_lock);
    let reader2 = Arc::clone(&mutex_lock);
    let writer = Arc::clone(&mutex_lock);

    // 데이터를 읽는 스레드 
    let read_thread1 = thread::Builder::new()
        .name(format!("read_thread-1"))  
        .spawn(move || {
            let data = reader1.lock().unwrap(); 
            info!("{:?}", data);
            thread::sleep(Duration::from_secs(2));
        }).unwrap();
    
    // 데이터를 쓰는 스레드 
    let writer_thread = thread::Builder::new()
        .name(format!("writer_thread"))  
        .spawn(move || {
            let mut data = writer.lock().unwrap();
            // 데이터 수정
            *data += 1;
            info!("{:?}", data);
            thread::sleep(Duration::from_secs(10)); 
        }).unwrap();
    
    // 데이터를 읽는 스레드 
    let read_thread2 = thread::Builder::new()
        .name(format!("read_thread-2"))  
        .spawn(move || {
            let data = reader2.lock().unwrap();
            info!("{:?}", data);
            thread::sleep(Duration::from_secs(2));
        }).unwrap();
    

    read_thread1.join().unwrap();
    writer_thread.join().unwrap();
    read_thread2.join().unwrap();

}


#[tokio::main]
async fn main() {
    
    // 커스텀 로거
    set_global_logger();

    info!("Test Start");
    
    mutex_test();

}

 

위의 코드는 세 개의 독립적인 스레드를 생성해 주는 것으로 시작한다.

 

첫 번째 스레드는 공유 데이터를 읽어주는 스레드이고

 

두 번째 스레드는 공유 데이터에 쓰기를 수행하는 스레드이고

 

마지막 스레드는 첫 번째 스레드와 같이 공유 데이터를 읽어주는 스레드이다.

 

 

3개의 스레드를 한 번에 실행시킴으로써 멀티스레드 프로그래밍을 구현하고 있다.

 

Mutex Lock 이 어떤 식으로 작동하는지 알기 위해서,

 

각 스레드마다 sleep() 함수를 통해서,

 

각 스레드 작업 후에 대기시간을 걸어주었다.

 

해당 소스코드를 실행해 보면 아래와 같은 로그가 남는다.

 

날짜 데이터의 초(second)를 자세히 보기를 바란다.

 

 

 

해당 소스 코드 작동방식을 도식화하면 아래와 같다.

 

 

 

 

2) RWLock

 

이번에는 같은 알고리즘이지만, RWLock으로 구현해 줬을 때는

 

Mutex와 어떤 다른 양상을 띠는지 알아보자.

 

소스코드는 아래와 같다.

 

fn rw_lock_test() {

    let lock = Arc::new(RwLock::new(0));
    
    let writer = Arc::clone(&lock);
    let reader1 = Arc::clone(&lock);
    let reader2 = Arc::clone(&lock);

    // 쓰기 스레드
    let write_thread = thread::Builder::new()
        .name(format!("write_thread"))  
        .spawn(move || {
            let mut w = writer.write().unwrap();
            info!("Writer starts writing");
            *w += 1;
            info!("Writer finished writing");
            thread::sleep(Duration::from_secs(3)); // 쓰기 작업 시간 지연
        }).unwrap();
    
    // 읽기 스레드 1
    let read_thread1 = thread::Builder::new()
        .name(format!("read_thread1"))  
        .spawn(move || {
            let r = reader1.read().unwrap();
            info!("Reader 1 read value: {}", *r);
            thread::sleep(Duration::from_secs(2));
        }).unwrap();
    
    // 읽기 스레드 2
    let read_thread2= thread::Builder::new()
        .name(format!("read_thread2"))  
        .spawn(move || {
            let r = reader2.read().unwrap();
            info!("Reader 2 read value: {}", *r);
            thread::sleep(Duration::from_secs(2));
        }).unwrap();

    write_thread.join().unwrap();
    read_thread1.join().unwrap();
    read_thread2.join().unwrap();

}



#[tokio::main]
async fn main() {
    
    // 커스텀 로거
    set_global_logger();

    info!("Test Start");
    
    rw_lock_test();
}

 

위의 알고리즘은 Mutex 때와 비슷한 알고리즘이지만,

 

수정된 점은 첫 번째 스레드가 쓰기 잠금을 수행해 주는 스레드이고

 

두 번째, 세 번째 스레드는 읽기 잠금을 수행해 주는 스레드이다.

 

소스코드 실행결과에 대한 로그는 아래와 같다.

 

 

로그내용에서 Mutex Lock 과의 차이점을 볼 수 있다.

 

쓰기 잠금에서는 다른 스레드의 접근을 허용하지 않는데,

 

읽기 잠금에서는 배타적 잠금 없이 다른 스레드의 읽기를 허용하고 있다.

 

해당 로직을 도식화하면 아래와 같다.

 

 

 

 

 

RWLock  vs  Mutex 요약

 

1) 동시 읽기

 

RWLock은 여러 스레드가 동시에 데이터를 읽는 것을 허용하는 반면,

 

Mutex는 이를 허용하지 않는다.

 

Mutex는 읽기와 쓰기 모두에 대해 독점적 접근을 요구한다.

 

 

2) 성능

 

RWLock은 읽기가 빈번히 이루어지고

 

쓰기가 상대적으로 드문 경우 성능 이점을 제공할 수 있다.

 

반면, Mutex는 모든 접근에 대해 동일한 수준의 제한을 가하므로,

 

읽기 작업이 많은 경우 성능 저하가 발생할 수 있다.

 

 

3) 사용 시나리오

 

데이터의 읽기와 쓰기 빈도가 크게 다른 경우 RWLock이 더 적합할 수 있다.

 

데이터에 대한 접근이 대체로 쓰기 작업으로 이루어지거나,

 

읽기와 쓰기의 빈도가 비슷하면 Mutex가 더 적합할 수 있다.

반응형