Rust - Interior Mutability?

Migo·2023년 8월 21일

rust

목록 보기
2/3
post-thumbnail

Conservative nature of Rust

Mutability

러스트는 값 변경에 대해 굉장히 보수적이다.

가령, 아래와 같은 코드는 동작하지 않는다. 값에 대한 onwer인 name이 변경가능한지 (mutable한지) 명시해주지 않았기 때문이다.

fn main(){
	let name = String::from("미고");
	name.push_str(" is the king of Rust"); 
    //cannot borrow `name` as mutable, as it is not declared as mutable
	//cannot borrow as mutable
}

이러한 보수성은 Rust코드를 안전하고, 정확하게 만들지만, 어플리케이션 가동 중 내내 사용되야하는 '전역적' 특성을 지닌 값들에 대해서는 공동 소유권을 인정해줘야 하는 경우가 생긴다.


Co-Ownership

가령 Multi-threaded 코드를 작성해야하고, 각 쓰레드는 하나의 공유자원에 접근해야한다고 가정해보자.

fn main() {
    let program_name = String::from("My Program");
    let t1 = thread::spawn(move || run(process_name));
    let t2 = thread::spawn(move || run(process_name));
}

fn run(program_name: String) {
    let thread_id = thread::current().id();
    println!("{} running thread with id: {:?}", program_name, thread_id)
    
}

다시 한번, Rust는 다른 언어와 다르게 Heap memory에 값을 가지고 있는 pointer들에 대해 co-ownership을 원칙적으로는 허용하지 않는다. 따라서 program_namet1 쓰레드로 이동하게 되고, 같은 값을 다시 t2 쓰레드에 던질수는 없다. 따라서 위의 코드는 컴파일되지 않는다.

Heap memory에 값을 가지고 있는 pointer들?
왜 Object가 아니라 pointer라고 부를까? 왜냐하면 데이터의 구조형이 그러하기 떄문이다. 가령 String은 Vec<u8>을 내부값으로 wrapping한 구조체이고, Vec는 내부적으로 heap 메모리에 대한 pointer, len, capacity등의 메타데이터를 갖는 fat pointer이다. Rust에서 object라는 말은 행위(method)와 속성(attribute)의 합이라는 의미에의 object과는 괴리가 있다. 이 부분은 다른 포스트에서 기술하도록 한다.

그렇다면 어떻게 전역적 특성을 띈 값들을 관리할 수 있을까? 명시적으로 co-ownership이 가능함을 type으로서 컴파일러에게 말해주어야 한다.

fn main() {
    let program_name = Arc::new(String::from("My Program"));
    let mut threads = vec![];
    {
        let program_name = program_name.clone();
        threads.push(thread::spawn(move || run(program_name)));
    }
    {
        let program_name = program_name.clone();
        threads.push(thread::spawn(move || run(program_name)));
    }

	for t in threads {
        let _ = t.join();
    }
}

fn run(program_name: Arc<String>) {
    let thread_id = thread::current().id();
    println!("{} running thread with id: {:?}", program_name, thread_id)
}



Changing values that were borrowed

자, Co-ownership에 대한 부분을 살펴보았으니 이 시점에서 이런 생각이 들 수 있겠다.

각 쓰레드에서 공동 소유하고 있는 값에 대한 변경은?

프로그래밍을 조금이라도 접해본 사람이라면 해당 operation이 얼마나 많은 주의를 요하는지 알것이다. (Race condition hell..)

직관적으로, 러스트가 해당 operation을 허용해줄까? 그럴리가 없다.

fn run(program_name: Arc<String>) {
    let thread_id = thread::current().id();
    println!("{} running thread with id: {:?}", program_name, thread_id)
    program_name.push_str("Hey~!");
}

에러 메세지는 다음과 같다.

cannot borrow data in an Arc as mutable
trait DerefMut is required to modify through a dereference, but it is not implemented for std::sync::Arc<std::string::String>

원천적으로, 공동소유권을 인정해주는 구조체인 Rc, Arc는 내부 값 변경에 대한 것을 허용하지 않는다.

구체적으로, Rc, Arc는 wrapping하고 있는 값에 대해 shared reference만을 노출시키고, mutable reference는 노출하지 않음으로써, 내부 값에 대한 변경을 원천차단한다.

공유하고 있는 리소스에 대한 무작위적인 변경은 어느 프로그래밍에서나 위험하다. 그리고 러스트는 원칙적으로, Single-onwership 등의 제한조건을 통해 해당 문제가 일어나지 않도록 하였다.

하지만, 데이터베이스 접근을 위한 커넥션 풀이나, 다른 기타 자원들에 대한 재사용성을 위해, 어플리케이션 레벨에서 관리되어야하는 리소스가 있고, 그것에 대한 변경이 불가피한 경우가 존재한다. 그리고 그런 위험한 operation 들에 대해, 러스트는 단 한가지 매커니즘을 제공한다.

Interior Mutability

자, 문제상황을 다시 한번 짚어보자.

어떻게하면 shared reference을 통해 값을 변경할 수 있을까?

저 질문은 "어떻게 하면, shared reference로 부터 mutable reference를 얻어낼 수 있을까?" 와 같은 질문이다.

이를 제공하는 Interface는 오직 std::cell::UnsafeCell<T> 혹은 그의 기출변형들을 통해서만 노출된다.

Unsafe?
러스트에서 Unsafe는, 그것이 반드시 메모리적으로 위험하다는 것을 의미하지 않는다. 다만 위험성을 내포할 수 있다는 Marker에 가깝다고 할 수 있다. standard library를 통해 유저로서 사용하는 모든 interior mutability관련 구조체들은, 내부적으로 std::cell::UnsafeCell<T>를 사용하지만(그리고 해당 구조체는 실제로 unsafe block안에서만 동작할 수 있지만, 해당 구조체를 랩핑하고 있는 다른 구조체는 safe한 interface를 통해 안전성이 검증되었음 마크를 찍고 나온다고 생각하면 좋다.

내부적으로 std::cell::UnsafeCell<T>를 갖는 구조체중 대표적으로,Mutex를 통해, 해당 문제를 해결해보자.

fn main() {
    let program_name = Arc::new(Mutex::new(String::from("My Program")));
    let mut threads = vec![];
    {
        let program_name = program_name.clone();
        threads.push(thread::spawn(move || run(program_name)));
    }
    {
        let program_name = program_name.clone();
        threads.push(thread::spawn(move || run(program_name)));
    }

    for t in threads {
        let _ = t.join();
    }
}

fn run(program_name: Arc<Mutex<String>>) {
    let thread_id = thread::current().id();
    let mut p_name = program_name.lock().unwrap();
    p_name.push_str("!!");
    println!("{} running thread with id: {:?}", p_name, thread_id);
}

위 프로그램의 출력 결과는 다음과 같다.

My Program!! running thread with id: ThreadId(3)
My Program!!!! running thread with id: ThreadId(2)

그렇다면 정말 Mutex가 정말 내부적으로 std::cell::UnsafeCell<T>를 갖는지 확인해보자.

#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "Mutex")]
pub struct Mutex<T: ?Sized> {
    inner: sys::Mutex,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

보이다 시피, dataUnsafeCell<T>를 값으로 갖는 필드이다.

이러한 Interior Mutability에 대한 이해는 Rust를 통해 Concurrency 문제를 해결함에 있어 필수적인 요소이며, 그것의 실제 구현체는 Mutex외에도 RwLock, Cell 등이 있다.

profile
Dude with existential crisis

0개의 댓글