Rc<T>, 참조 카운트 스마트 포인터

Beginner

This tutorial is from open-source community. Access the source code

소개

Rc, 참조 카운트 스마트 포인터에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 값에 대한 참조 수를 추적하고 소유자가 없을 때만 정리되도록 보장하여 Rust 에서 Rc (참조 카운팅) 를 사용하여 값의 다중 소유자를 활성화하는 방법을 살펴봅니다.

Rc<T>, 참조 카운트 스마트 포인터

대부분의 경우 소유권은 명확합니다. 주어진 값을 정확히 어떤 변수가 소유하는지 알고 있습니다. 그러나 단일 값이 여러 소유자를 가질 수 있는 경우가 있습니다. 예를 들어, 그래프 데이터 구조에서 여러 간선이 동일한 노드를 가리킬 수 있으며, 해당 노드는 개념적으로 이를 가리키는 모든 간선에 의해 소유됩니다. 노드는 이를 가리키는 간선이 없고 소유자가 없는 경우에만 정리되어야 합니다.

Rust 타입 Rc<T>를 사용하여 여러 소유권을 명시적으로 활성화해야 합니다. 이는 참조 카운팅의 약어입니다. Rc<T> 타입은 값에 대한 참조 수를 추적하여 해당 값이 여전히 사용 중인지 여부를 결정합니다. 값에 대한 참조가 0 개인 경우, 참조가 무효화되지 않고 값을 정리할 수 있습니다.

Rc<T>를 거실의 TV 라고 상상해 보세요. 한 사람이 TV 를 보기 위해 들어오면 TV 를 켭니다. 다른 사람들도 방에 들어와 TV 를 볼 수 있습니다. 마지막 사람이 방을 나가면 더 이상 사용하지 않으므로 TV 를 끕니다. 다른 사람들이 TV 를 보고 있는 동안 누군가 TV 를 끄면, 남아있는 TV 시청자들로부터 항의가 있을 것입니다!

프로그램의 여러 부분에서 힙에 일부 데이터를 할당하여 읽고 싶고, 컴파일 시간에 어떤 부분이 데이터를 마지막으로 사용할지 결정할 수 없을 때 Rc<T> 타입을 사용합니다. 어떤 부분이 마지막으로 끝나는지 알고 있다면, 해당 부분을 데이터의 소유자로 만들 수 있으며, 컴파일 시간에 적용되는 일반적인 소유권 규칙이 적용됩니다.

Rc<T>는 단일 스레드 시나리오에서만 사용됩니다. 16 장에서 동시성에 대해 논의할 때, 멀티스레드 프로그램에서 참조 카운팅을 수행하는 방법을 다룰 것입니다.

Rc<T>를 사용하여 데이터 공유하기

Listing 15-5 의 cons list 예제로 돌아가 보겠습니다. Box<T>를 사용하여 정의했음을 기억하세요. 이번에는 세 번째 리스트를 모두 공유하는 두 개의 리스트를 만들 것입니다. 개념적으로, 이것은 그림 15-3 과 유사합니다.

그림 15-3: 세 번째 리스트 a를 공유하는 두 개의 리스트 bc

510을 포함하는 리스트 a를 만들 것입니다. 그런 다음 3으로 시작하는 b4로 시작하는 c의 두 개의 리스트를 더 만들 것입니다. bc 리스트는 모두 510을 포함하는 첫 번째 a 리스트로 이어집니다. 즉, 두 리스트 모두 510을 포함하는 첫 번째 리스트를 공유합니다.

Listing 15-17 에 표시된 것처럼 Box<T>를 사용하여 List를 정의하여 이 시나리오를 구현하려고 하면 작동하지 않습니다.

파일 이름: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
  1 let b = Cons(3, Box::new(a));
  2 let c = Cons(4, Box::new(a));
}

Listing 15-17: 세 번째 리스트의 소유권을 공유하려는 Box<T>를 사용하는 두 개의 리스트를 가질 수 없음을 보여줍니다.

이 코드를 컴파일하면 다음과 같은 오류가 발생합니다.

error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which
does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

Cons 변형은 자신이 보유한 데이터를 소유하므로 b 리스트 [1]를 만들 때 ab로 이동하고 ba를 소유합니다. 그런 다음 c [2]를 만들 때 a를 다시 사용하려고 하면 a가 이동되었기 때문에 사용할 수 없습니다.

Cons의 정의를 참조를 보유하도록 변경할 수 있지만, 그러면 lifetime 매개변수를 지정해야 합니다. lifetime 매개변수를 지정하면 리스트의 모든 요소가 전체 리스트만큼 오래 지속될 것이라고 지정하는 것입니다. 이것은 Listing 15-17 의 요소와 리스트의 경우이지만 모든 시나리오에서 그런 것은 아닙니다.

대신, Listing 15-18 에 표시된 것처럼 Box<T> 대신 Rc<T>를 사용하도록 List의 정의를 변경할 것입니다. 이제 각 Cons 변형은 값과 List를 가리키는 Rc<T>를 보유합니다. b를 만들 때 a의 소유권을 가져가는 대신, a가 보유하고 있는 Rc<List>를 복제하여 참조 수를 1 에서 2 로 늘리고 ab가 해당 Rc<List>의 데이터를 공유하도록 합니다. 또한 c를 만들 때 a를 복제하여 참조 수를 2 에서 3 으로 늘립니다. Rc::clone을 호출할 때마다 Rc<List> 내의 데이터에 대한 참조 카운트가 증가하고, 해당 데이터에 대한 참조가 0 개가 아닌 한 데이터는 정리되지 않습니다.

파일 이름: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
1 use std::rc::Rc;

fn main() {
  2 let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
  3 let b = Cons(3, Rc::clone(&a));
  4 let c = Cons(4, Rc::clone(&a));
}

Listing 15-18: Rc<T>를 사용하는 List의 정의

Rc<T>를 범위 내로 가져오기 위해 use 문을 추가해야 합니다 [1] (prelude 에 없기 때문입니다). main에서 510을 보유하는 리스트를 만들고 a [2]의 새로운 Rc<List>에 저장합니다. 그런 다음 b [3]와 c [4]를 만들 때 Rc::clone 함수를 호출하고 aRc<List>에 대한 참조를 인수로 전달합니다.

Rc::clone(&a) 대신 a.clone()을 호출할 수도 있었지만, Rust 의 규칙은 이 경우 Rc::clone을 사용하는 것입니다. Rc::clone의 구현은 대부분의 타입의 clone 구현과 같이 모든 데이터를 깊이 복사하지 않습니다. Rc::clone 호출은 참조 카운트만 증가시키며, 이는 많은 시간이 걸리지 않습니다. 데이터의 깊은 복사는 많은 시간이 걸릴 수 있습니다. 참조 카운팅에 Rc::clone을 사용함으로써, 깊이 복사 종류의 clone 과 참조 카운트를 증가시키는 종류의 clone 을 시각적으로 구별할 수 있습니다. 코드에서 성능 문제를 찾을 때, 깊이 복사 clone 만 고려하면 되고 Rc::clone 호출은 무시할 수 있습니다.

Rc<T> 복제는 참조 카운트를 증가시킵니다

Listing 15-18 의 작동 예제를 변경하여 aRc<List>에 대한 참조를 생성하고 삭제할 때 참조 카운트가 변경되는 것을 볼 수 있습니다.

Listing 15-19 에서 main을 변경하여 리스트 c 주변에 내부 범위를 갖도록 합니다. 그러면 c가 범위를 벗어날 때 참조 카운트가 어떻게 변경되는지 확인할 수 있습니다.

파일 이름: src/main.rs

--snip--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!(
        "count after creating a = {}",
        Rc::strong_count(&a)
    );
    let b = Cons(3, Rc::clone(&a));
    println!(
        "count after creating b = {}",
        Rc::strong_count(&a)
    );
    {
        let c = Cons(4, Rc::clone(&a));
        println!(
            "count after creating c = {}",
            Rc::strong_count(&a)
        );
    }
    println!(
        "count after c goes out of scope = {}",
        Rc::strong_count(&a)
    );
}

Listing 15-19: 참조 카운트 출력

참조 카운트가 변경되는 프로그램의 각 지점에서 Rc::strong_count 함수를 호출하여 얻는 참조 카운트를 출력합니다. 이 함수는 count 대신 strong_count로 명명되었습니다. Rc<T> 타입에는 또한 weak_count가 있기 때문입니다. "Weak<T>를 사용하여 참조 사이클 방지"에서 weak_count가 무엇에 사용되는지 살펴보겠습니다.

이 코드는 다음을 출력합니다.

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

aRc<List>는 초기 참조 카운트가 1 임을 알 수 있습니다. 그런 다음 clone을 호출할 때마다 카운트가 1 씩 증가합니다. c가 범위를 벗어나면 카운트가 1 씩 감소합니다. 참조 카운트를 증가시키기 위해 Rc::clone을 호출해야 하는 것처럼 참조 카운트를 감소시키기 위해 함수를 호출할 필요는 없습니다. Drop 트레이트의 구현은 Rc<T> 값이 범위를 벗어날 때 자동으로 참조 카운트를 감소시킵니다.

이 예제에서 볼 수 없는 것은 bamain의 끝에서 범위를 벗어날 때 카운트가 0 이 되고 Rc<List>가 완전히 정리된다는 것입니다. Rc<T>를 사용하면 단일 값이 여러 소유자를 가질 수 있으며, 카운트는 소유자가 여전히 존재하는 한 값이 유효하게 유지되도록 보장합니다.

불변 참조를 통해 Rc<T>는 프로그램의 여러 부분 간에 읽기 전용으로 데이터를 공유할 수 있습니다. Rc<T>가 여러 가변 참조도 허용했다면, 4 장에서 논의한 차용 규칙 중 하나를 위반할 수 있습니다. 동일한 위치에 대한 여러 가변 차용은 데이터 경합 및 불일치를 유발할 수 있습니다. 하지만 데이터를 변경할 수 있는 것은 매우 유용합니다! 다음 섹션에서는 내부 가변성 패턴과 이 불변성 제한 사항을 처리하기 위해 Rc<T>와 함께 사용할 수 있는 RefCell<T> 타입을 논의합니다.

요약

축하합니다! 참조 카운트 스마트 포인터 (Rc) 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.