Dev/Language

Rc 타입과 Weak 타입 - Rust 프로그래밍

TaeGit 2022. 10. 29. 20:49

Rust Programming Language

목차

  • Rc 타입과 Weak 타입
  • Rc 타입 사용하기
  • Weak 타입 사용하기
  • 순환 참조의 문제

 

 

Rc 타입과 Weak 타입

소유권 규칙에 따라 Rust에서 어떤 값은 여러 소유자를 가질 수 없습니다. Reference Counted를 의미하는 Rc는 힙 메모리에 할당된 타입 T 값의 소유권을 공유할 수 있게 해주는 타입입니다. 즉, 스마트 포인터 Rc를 사용하면 타입 T의 값에 대한 여러 개의 소유자를 만들 수 있습니다.

 

기본적으로, Rc 타입Clone Trait을 구현하고 있고 clone을 호출해서 T 값에 대한 새로운 포인터를 생성하며, 모든 Rc 포인터가 해제되면 메모리에 할당된 T 값이 drop되는 구조입니다. Rust에서 공유된 참조자는 수정할 수 없는데, Rc 타입 또한 예외가 아니며 일반적인 방법으로는 mutable한 참조를 얻을 수 없습니다. 만약, mutable 한 참조가 필요하면 Cell 타입이나 RefCell 타입을 함께 사용해야 합니다.

 

Rc 타입은 원자적이지 않은 참조 카운팅으로 오버헤드가 낮은 장점이 있지만 단일 스레드에서만 사용 가능합니다. 따라서, Rc 타입은 스레드 간 전송을 위한 TraitSend 타입을 구현하지 않습니다. 한편, 원자적 참조 카운팅 타입인 Arc(Atomic Reference Counted)가 있습니다. Arc 타입은 다중 스레드 간 사용이 가능하지만 Rc 타입보다 오버헤드가 큰 단점이 있습니다.

 

std::rc 모듈에는 Rc 타입과 더불어 약한 참조인 Weak 타입이 존재합니다. Rc 타입downgrade해서 Weak<T> 타입을 얻을 수 있고, Weak 타입upgrade해서 Option<Rc<T>> 타입을 얻을 수 있습니다. Weak 타입에서 Rc 타입을 얻고자 upgrade를 사용할 때, T 값이 이미 drop 되었다면 None을 리턴합니다. 이 말은 Rc 타입과 다르게 Weak 타입은 메모리에 할당된 타입 T의 값이 살아있도록 유지시키지 않는다는 것을 의미합니다.


즉, Rc 타입의 강한 참조 카운트가 0이 되면 T 값이 drop 되는데, 약한 참조 카운트를 의미하는 Weak 타입은 영향을 미치지 않습니다.

앞서 설명했듯, Rc 타입이 갖고 있는 강한 참조 카운트가 0이 되지 않으면 T 값은 drop 되지 않습니다. 이는 T 값이 결코 drop 될 수 없는 순환 참조를 야기할 수도 있습니다. T 값의 drop에 영향을 미치지 않는 Weak 타입은 이런 문제의 상황에서 유용하게 사용될 수 있습니다. 자세한 내용은 아래에서 코드와 함께 살펴보겠습니다.

 

 

Rc 타입 사용하기

fn main() {
    let rc = Rc::new(10);
    println!("[ 1 ]");
    println!("value of rc => {}", rc);
    println!("strong count => {}", Rc::strong_count(&rc));
    println!("weak count => {}", Rc::weak_count(&rc));
}

Rc 타입은 new 함수를 통해 생성할 수 있습니다. 위 코드 3번째 라인에서 볼 수 있듯이 Rc 타입Deref Trait을 구현하고 있어 자동으로 역참조 됩니다. Rc가 생성될 때 strong_count 값은 1로 초기화됩니다.

 

아래 코드는 clone을 통해 타입 T 값에 대한 여러 개의 참조자를 만드는 것을 보여줍니다.

fn main() {
    let rc = Rc::new(10);
    println!("[ 1 ]");
    println!("value of rc => {}", rc);
    println!("strong count => {}", Rc::strong_count(&rc));
    println!("weak count => {}", Rc::weak_count(&rc));

    {
        let rc2 = rc.clone();
        println!("[ 2 ]");
        println!("value of rc2 => {}", rc2);
        println!("strong count => {}", Rc::strong_count(&rc));
        println!("weak count => {}", Rc::weak_count(&rc));

        let rc3 = Rc::clone(&rc);
        println!("[ 3 ]");
        println!("value of rc3 => {}", rc3);
        println!("strong count => {}", Rc::strong_count(&rc));
        println!("weak count => {}", Rc::weak_count(&rc));
    }

    println!("[ 4 ]");
    println!("strong count => {}", Rc::strong_count(&rc));
    println!("weak count => {}", Rc::weak_count(&rc));
}

Rc 타입Clone Trait을 구현하고 있고 rc.clone()Rc::clone() 두 가지 방식으로 호출할 수 있습니다. clone을 호출하면 타입 T 값에 대한 새로운 참조자가 생성되며, strong_count는 1 증가합니다. rc2와 rc3가 코드 블록을 벗어남에 따라 strong_count는 총 2가 감소해 마지막으로 출력되는 strong_count의 값은 1이 됩니다. 생성된 모든 Rc 타입이 drop 되면 strong_count가 0이 되면서 힙에 할당된 타입 T 값 역시 drop 됩니다.

 

 

Weak 타입 사용하기

Weak 타입에도 생성 메서드 new가 존재하지만, 인자로 어떠한 타입 T 값도 받지 않습니다. 즉, 타입 T에 대한 어떠한 값도 메모리에 할당되지 않습니다. 따라서, new로 새롭게 생성한 Weak 타입의 upgrade 메서드는 항상 None을 리턴합니다.

fn main() {
    let weak: Weak<i32> = Weak::new();
    assert!(weak.upgrade().is_none());
}

 

아래 코드에서 Rc 타입Weak 타입으로 downgrade 하고, Weak 타입Rc 타입으로 upgrade 하는 과정을 볼 수 있습니다. 예제를 실행해 strong_countweak_count 값의 변화를 확인해보세요.

fn main() {
    let rc = Rc::new(10);
    println!("[ 1 ]");
    println!("value of rc => {}", rc);
    println!("strong count => {}", Rc::strong_count(&rc));
    println!("weak count => {}", Rc::weak_count(&rc));

    let weak = Rc::downgrade(&rc);
    println!("[ 2 ]");
    println!("value of weak => {}", unsafe { &*weak.as_ptr() });
    println!("strong count => {}", Rc::strong_count(&rc));
    println!("weak count => {}", Rc::weak_count(&rc));

    if let Some(rc2) = Weak::upgrade(&weak) {
        println!("[ 3 ]");
        println!("value of rc2 => {}", rc2);
        println!("strong count => {}", Rc::strong_count(&rc));
        println!("weak count => {}", Rc::weak_count(&rc));
    } else {
        println!("강한 참조가 남아 있지 않습니다.");
    }
}

9번째 줄에서 값을 출력하는 방식이 Rc와 다른 것을 확인할 수 있습니다. Weak 타입Rc 타입과 다르게 Deref Trait을 구현하고 있지 않기 때문에 자동으로 역참조가 일어나지 않습니다. 또한, as_ptr() 메서드를 통해 T 값에 접근할 수 있지만, 아직 타입 T 값이 메모리에서 drop 되지 않았다는 것을 알 수 없기 때문에 unsafe 키워드를 사용해야 합니다. 그래서 개발자는 Weak 참조자가 가리키는 값이 아직 유효하다는 것을 보장할 수 있을 때 사용해야 합니다.

 

Weak 타입Rc 타입으로 upgrade 할 수 있는데, downgrade와 달리 upgrade는 Option<Rc> 을 반환합니다. 이는, 앞서 설명했듯이 Weak 타입이 메모리에 할당된 타입 T 값의 유효성을 보장하지 않기 때문입니다. 만약 strong_count가 0이 되어 타입 T 값이 drop 된 상태라면, upgrade 메서드는 None을 반환할 것입니다.

 

 

순환 참조의 문제

Rc 타입 간에는 순환 참조 문제가 발생할 수 있는데, 이를 해결하기 위해서 Weak 타입을 사용할 수 있습니다. 트리 데이터 구조 예제 코드를 통해 순환 참조가 일어날 수 있는 상황에서 Weak 타입을 활용하는 방법을 알아보겠습니다.

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

먼저, 트리의 노드 구조체를 만듭니다. 이 노드는 하나의 값과 자식 노드들의 참조자들을 가지고 있습니다. 여기서 자식 노드는 Rc 타입으로 소유권을 공유하고 직접 접근할 수 있습니다. 또한 자식 노드가 수정될 수 있도록 RefCell<T> 타입으로 감쌌습니다.

 

이제, 이 노드 구조체를 이용해 leafleaf를 자식 노드로 가지는 branch를 만들어 보겠습니다.

fn main() {
    let leaf = Rc::new(
        Node {
            value: 3,
            children: RefCell::new(vec![]),
        }
    );

    let branch = Rc::new(
        Node {
            value: 5,
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        }
    );
}

leaf는 자식이 없는 Node이고, branchleaf를 자식으로 갖는 Node입니다. 우리는 이제 branch.children을 통해 branch에서 leaf로 접근할 수 있습니다. 하지만, leaf가 부모 노드에 대한 참조자를 알지 못하기 때문에 leaf에서 branch로는 접근이 불가능한 상황입니다. 이를 위해 자식 노드가 부모 노드로 접근할 수 있도록 하는 parent 참조자를 추가해야 합니다.

 

우리가 parent 타입을 추가함으로써 leaf가 부모인 branch를 참조하고, branch가 자식인 leaf를 참조한다는 것을 쉽게 생각해볼 수 있습니다. 이때 parent의 타입을 children과 같이 Rc 타입으로 만든다면, strong_count 값이 0이 될 수 없는 순환 참조 문제를 야기할 수 있습니다. 따라서, 우리는 순환 참조 문제를 피하기 위해 parent 타입을 Weak 타입으로 만들 것입니다.

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

이제, 노드는 부모 노드를 소유하지는 않지만 참조할 수 있게 되었습니다.

 

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

16번째 줄에서 leaf 노드의 parentbranch 노드의 Weak 참조자를 넣어 주는 것을 볼 수 있습니다.


아래는 위 코드를 출력한 결과입니다.

branch, leaf 출력 결과

결과를 보면 Weak 참조자(Weak)로 출력되는 것을 알 수 있고, 무한 출력이 없다는 것은 순환 참조를 야기하지 않고 있다는 것을 의미합니다.

 

 

 

References

 

Rc in std::rc - Rust

Constructs a new Rc with uninitialized contents, with the memory being filled with 0 bytes, returning an error if the allocation fails See MaybeUninit::zeroed for examples of correct and incorrect usage of this method. #![feature(allocator_api, new_uninit)

doc.rust-lang.org

 

 

Reference Cycles Can Leak Memory - The Rust Programming Language

Rust’s memory safety guarantees make it difficult, but not impossible, to accidentally create memory that is never cleaned up (known as a memory leak). Preventing memory leaks entirely is not one of Rust’s guarantees, meaning memory leaks are memory sa

doc.rust-lang.org