#28 공유 상태와 동시성 트레이트

Pt J·2020년 9월 18일
0

[完] Rust Programming

목록 보기
31/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

어떤 자료를 다른 쓰레드로 전달하기 위해서는 채널을 사용하면 된다.
하지만 만약 이 쓰레드에서도 사용하면서 다른 쓰레드도 사용하도록 공유하고 싶다면
우리는 채널이 아니라 다른 방식을 사용해야 한다.
그런데 이와 같이 어떤 자료를 여러 쓰레드에서 공유하려면
다중 소유권이 적용되어 그만큼 복잡도가 높아진다.

Mutex Mutual Exclusion

mutex는 한 시점에 단 하나의 쓰레드만 어떤 자료에 접근하도록 하기 위해 필요한 녀석이다.
mutex의 자료에 접근할 땐 lock을 걸어야 하고, 사용이 완료된 후 lock을 해제해야 한다.
lock은 자료를 어느 쓰레드가 사용하고 있는지 추적하는 용도로 사용되는 자료구조다.
만약 다른 쓰레드에 의해 lock이 걸려 있다면 그 자료는 접근하지 못한다.

lock은 다음과 같은 규칙을 가지고 있다.

  • 자료를 사용하기 전에는 반드시 lock 획득을 시도해야 한다.
    You must attempt to acquire the lock before using the data.
  • mutex가 보호하는 자료의 사용이 끝나면 다른 쓰레드에서 획득할 수 있도록 반드시 lock을 해제해야 한다.
    When you’re done with the data that the mutex guards, you must unlock the data so other threads can acquire the lock.

여타 언어들에서는 lock을 해제하는 부분을 빼먹거나
다른 mutex의 lock을 해제하는 등의 실수를 할 수 있지만
Rust는 자료형 검사 및 소유권 검사를 통해 이러한 실수를 방지할 수 있다.

mutex도 다른 자료형들이 그렇듯 연관함수 new를 통해 생성한다.
생성할 때 인자로 mutex에 의해 보호될 자료를 전달한다.

lock을 획득하기 위해서는 Mutex<T>의 메서드 lock을 호출해야 한다.
lockLockResult 값을 반환하며
lock을 획득했다면 스마트 포인터 MutexGuard 값에 접근할 수 있게 된다.
MutexGuadrd는 스마트 포인터답게 Deref 트레이트와 Drop 트레이트를 구현하고 있다.
이 녀석은 범위를 벗어나서 소유권을 잃으면 lock이 자동으로 해제되어
실수로 lock을 해제하지 않아 발생하는 문제를 사전 차단한다.

이렇게 사용하는 게 큰 의미는 없지만 단일 쓰레드에서 mutex를 사용하는 예제를 작성해보자.
lock이 자동으로 해제되도록 하기 위해 lock은 중괄호 코드 블럭 안에서 호출한다.

peter@hp-laptop:~/rust-practice/chapter16$ cargo new single_mutex
     Created binary (application) `single_mutex` package
peter@hp-laptop:~/rust-practice/chapter16$ cd single_mutex/
peter@hp-laptop:~/rust-practice/chapter16/single_mutex$ vi src/main.rs

src/main.rs

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);
.
    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m);
}
peter@hp-laptop:~/rust-practice/chapter16/single_mutex$ cargo run
   Compiling single_mutex v0.1.0 (/home/peter/rust-practice/chapter16/single_mutex)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/single_mutex`
m = Mutex { data: 6 }
peter@hp-laptop:~/rust-practice/chapter16/single_mutex$ 

따로 unlock을 해주지 않았지만 자동으로 lock이 해제되어 m에 접근 가능한 것을 확인할 수 있다.

이제 본격적으로 여러 개의 쓰레드에서 mutex를 사용하는 방법을 알아보자.
mutex 변수도 여타 Rust 자료형과 마찬가지로 소유권에 의해 관리된다.
즉, 이걸 그대로 어떤 쓰레드에 넣어버리면 소유권이 이전되어 다른 쓰레드에 전달할 수 없다.
메시지 전달에서는 이 문제를 해결하기 위해 송신자를 복제하였지만
mutex는 그것이 가진 값이 서로 동기화 되어야 하므로 복제로는 해결되지 않는다.
마음 같아서는 스마트 포인터 Rc<T>를 사용하고 싶지만
이 녀석은 멀티 쓰레드 환경에서 안전하지 않아 사용할 수 없다.

다행히도 Rust 표준 라이브러리에는
멀티 쓰레드 환경에서 Rc<T>처럼 사용할 수 있는 자료형이 존재한다.
Rc<T>에 원자성Atomic을 더해 Arc<T>라고 한다.
원자성을 보장하기 위한 코드는 그렇지 않은 코드보다 약간의 성능 저하가 존재하는데
원자성이 필요하지 않은 단일 쓰레드에서는 성능 저하 없이 Rc<T>를,
그것이 필요한 멀티 쓰레드에서는 성능 저하를 감안하고 Arc<T>를 사용하는 것이다.

Arc<T>를 사용하는 방법은 Rc<T>와 크게 다르지 않다.
둘은 같은 API를 제공하고 있으며 내부적으로 원자성 보장 여부만 다를 뿐이다.

여러 쓰레드에서 mutex를 공유하는 예제를 살펴보자.
이 예제는 각각의 쓰레드에서 counter의 값을 1씩 증가시킨다.

peter@hp-laptop:~/rust-practice/chapter16/single_mutex$ cd ..
peter@hp-laptop:~/rust-practice/chapter16$ cargo new mutex_counter
     Created binary (application) `mutex_counter` package
peter@hp-laptop:~/rust-practice/chapter16$ cd mutex_counter/
peter@hp-laptop:~/rust-practice/chapter16/mutex_counter$ vi src/main.rs

src/main.rs

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
peter@hp-laptop:~/rust-practice/chapter16/mutex_counter$ cargo run
   Compiling mutex_counter v0.1.0 (/home/peter/rust-practice/chapter16/mutex_counter)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/mutex_counter`
Result: 10
peter@hp-laptop:~/rust-practice/chapter16/mutex_counter$ 

10번의 반복문에서 각각의 쓰레드가 counter 값을 1씩 잘 증가시킨 것 같다.

잘 보면, 우리는 counter를 불변으로 선언했는데 그것이 가진 값을 변경하고 있다.
이것은 Mutex<T>RefCell<T>처럼 내부 가변성을 지원하기 때문에 가능한 일이다.

mutex를 사용하면서 주의할 점은,
Rust는 여러 가지 안전성을 보장해주지만 데드락은 막아주지 않는다는 점이다.
데드락이 발생할 수 있는 지점에 대해서는 우리가 직접 처리해주어야 한다.

동시성 기능의 구현

Rust의 동시성은 언어 자체가 지원하는 것이 아니라 Mutex와 같은 표준 라이브러리가 지원한다.
필요에 따라 동시성 기능을 위한 라이브러리를 직접 구현하거나
다른 사람이 crates.io에 올려 놓은 것을 사용할 수 있다.

동시성 기능을 구현하기 위해 사용할 수 있는 유용한 트레이트로 SendSync가 있다.
이것은 Rust 언어 차원에서 지원하는 녀석들이다.
이 녀석들은 단지 "이런 특성을 가진 자료형이다"를 알리고
내부적으로 이를 위한 처리를 해주기 위한 것들일 뿐,
따로 구현해야 할 메서드는 존재하지 않는다.
이런 트레이트를 마커Marker 트레이트라고 한다.

Send 트레이트

Send 트레이트를 구현하는 자료형은 소유권이 다른 쓰레드로 이동할 수 있다.
Rust의 대부분의 자료형은 Send 트레이트를 구현한다.
하지만 Rc<T>와 같이 그렇지 않은 자료형도 존재한다는 것을 주의하자.
Send 트레이트를 구현하지 않은 자료형을 다른 쓰레드로 넘기려고 하면
다음과 같은 오류가 발생할 것이다.

the trait Send is not implemented for 자료형

Send 트레이트를 구현하는 자료형만의 조합으로 정의된 자료형은
자동으로 Send 트레이트를 구현한 것으로 취급된다.

Sync 트레이트

Sync 트레이트를 구현하는 자료형은 여러 쓰레드에서 안전하게 참조할 수 있다.
Send 트레이트와 마찬가지로 이 녀석도 Rust의 대부분의 자료형이 구현하고 있다.
그리고 Sync 트레이트를 구현하는 자료형만의 조합으로 정의된 자료형이
자동으로 Sync 트레이트를 구현한 것으로 취급되는 것도 마찬가지다.

우리가 지금까지 다루면서 멀티 쓰레드 환경에서 사용할 수 없다고 한 녀석들은
위 트레이트들이 구현되어 있지 않기 때문이다.

우리가 직접 동시성 기능을 가진 자료형을 만들 수 있기는 하지만 추천하지는 않는다.
안전성을 위해 고려해야 할 것도 많고 그닥 생산성 있는 일은 아니다.
기존에 존재하는 동시성 기능을 사용하도록 하자.

이 포스트의 내용은 공식문서의 16장 3절 Shared-State Concurrency & 16장 4절 Extensible Concurrency with the Sync and Send Traits에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글