RUST

[RUST] Rc, Arc 란

ssh9308 2024. 4. 30. 23:36
반응형

Rust에서 Arc와 Rc는 둘 다 참조 카운팅 방식의 스마트 포인터이다.

 

이들은 메모리 관리를 자동화하여,

 

동적으로 할당된 데이터의 생명주기를 관리하는 데 사용된다.

 

그러나 두 스마트 포인터는

 

사용되는 환경(멀티 스레드  대 단일 스레드)에 따라 구분된다.

 

 

 

Rc (Reference Counted)

 

Rc는 Reference Counted의 약자로,

 

단일 스레드 환경에서만 사용되도록 설계된 참조 카운팅 스마트 포인터이다.

 

Rc는 여러 부분에서 동일한 데이터에 대한 소유권을 공유할 수 있게 해 준다.

 

이는 특정 데이터에 대한 여러 소유자를 허용하고,

 

그 데이터가 더 이상 필요하지 않을 때 자동으로 메모리를 해제한다.

 

Rc는 Thread-safe(스레드-안전) 하지 않으며,

 

따라서 멀티 스레드 환경에서는 사용할 수 없다.

 

Rc 내부의 참조 카운팅 메커니즘은 동시 접근을 고려하지 않아 성능이 좋지만,

 

멀티 스레드에서는 데이터 경쟁(race condition)을 일으킬 수 있다.

 

 

 

Rc 구조

 

기본적으로 RUST 단일스레드환경에서 아래와 같은 소스코드를 동작시켜 보자.

 

async fn main() {
    
    let s1 = String::from("hello");
    let s2 = s1;
    
}

 

 

소스코드를 실행하면, 메모리의 상태는 아래의 그림과 같다.

 

RUST에서는 "소유권"이라는 개념이 존재한다.

 

처음에는 s1 이 Heap에 존재하는 "hello" 스트링 데이터에 대한 소유권이 있었지만,

 

s2에게 소유권을 건네줬기 때문에,

 

더 이상 s1 변수를 유효한 포인터로서의 역할을 수행할 수 없다.

 

 

하지만, Rc<T> 를 사용하면 소유권을 공유할 수 있다.

 

async fn main() {

    let s1 = Rc::new(String::from("hello"));

    let s2 = Rc::clone(&s1);
    let s3 = Rc::clone(&s1);
    
}

 

위의 소스코드를 실행한 후 메모리의 상태는 아래와 같다.

 

 

 

여기서 유의할 점은 Rc::clone()을 수행한다고 해서

 

깊은 복사가 일어나지는 않는다는 것이다.

 

Rc::clone()의 의미는 Rc <T> 객체 내에 있는 Ref count (참조카운트)를

 

하나 더 늘려주는 역할을 수행한다.

 

그리고 해당 Rc <T> 객체 내의 참조 카운트가 0이 되는 순간

 

해당 객체는 Heap 메모리에서 자동으로 정리가 된다.

 

 

 

 

Arc (Atomic Reference Counted)

 

Arc는 Atomic Reference Counted의 약자로,

 

멀티 스레드 환경에서 안전하게 사용할 수 있도록 설계된

 

참조 카운팅 스마트 포인터이다.

 Arc는 Rc와 유사하게 작동하지만,

 

참조 카운트를 증가시키고 감소시킬 때

 

원자적 연산을 사용하여 스레드 간의 안전을 보장한다.

 

Arc는 원자적 연산을 사용해 참조 카운트를 관리하기 때문에,

 

여러 스레드에서 동시에 참조 카운트를 수정해도 안전하다.

 

이로 인해 성능은 Rc보다 다소 떨어질 수 있으나,

 

멀티 스레드 환경에서 필수적이다.

 

 

Arc 구조

 

RUST 도 기본적으로 Heap 영역은 모든 스레드가 공유하는 공간이다.

 

특정 스레드에서 발생한 데이터를

 

다른 스레드에서도 안전하게 사용해야 한다고 가정해 보자.

 

위에서 설명한, Rc <T>는 단일 스레드에서만

 

소유권을 공유해 줄 수 있고 멀티스레드 환경에서는 불가능하다.

 

물론 아래와 같이 소유권 문제를 회피해 줄 수는 있다.

 

 

async fn main() {

    let s1 = String::from("hello");

    for i in 0..2 {

        let s1_moved = s1.clone();

        let handle = thread::Builder::new()
            .name(format!("thread-{}", i))  // 스레드에 이름 지정
            .spawn(move || {
                info!("{:?}", s1_moved);
            })
            .unwrap();
        
        handle.join().unwrap();
            
    }
    
 }

 

 

위의 코드가 언뜻 보면 s1 데이터를

 

각 스레드에서 안전하게 공유하고 있는 게 사용하는 게 아닌가 하겠지만,

 

메모리 구조를 살펴보면 이러한 사고는 매우 잘못되었단 걸 알 수 있다.

 

 

사실상 2,3번 스레드에서는

 

깊은 복사가 수행되어 아예 독립적인 String 데이터를

 

가리키고 있는 것이다.

 

그렇다면, 안전한 방식으로 깊은 복사가 발생하지 않도록

 

여러 개의 스레드에서 하나의 데이터를 공유하려면

 

어떻게 해야 할까?

 

바로 Arc 스마트 포인터를 사용하는 것이다.

 

async fn main() {

    let s1 = Arc::new(String::from("hello"));

    for i in 0..2 {

        let s1_moved = Arc::clone(&s1);

        let handle = thread::Builder::new()
            .name(format!("thread-{}", i))  // 스레드에 이름 지정
            .spawn(move || {
                info!("{:?}", s1_moved);
            })
            .unwrap();
        
        handle.join().unwrap();
            
    }
}

 

위의 소스코드를 실행한 후 메모리의 상태는 아래와 같다.

 

 

 

위와 같이 코딩하게 되면 깊은 복사가 일어나지 않고,

 

각 스레드에서 하나의 데이터를 안전하게 공유할 수 있다.

 

Arc 객체는 참조 카운트를 사용하여

 

데이터의 소유권을 안전하게 공유할 수 있도록 한다.

 

Arc의 참조 카운트는 해당 Arc 인스턴스가

 

여러 소유자에게 소유되고 있을 때 그 수를 정확하게 반영한다.

 

또한 멀티 스레드 환경에서 안전하게 사용할 수 있도록 설계되어 있다.

 

참조 카운트의 증감 작업은 원자적(atomic) 연산을 통해 이루어지므로,

 

여러 스레드에서 동시에 이러한 작업이 수행되더라도

 

데이터 경쟁이 발생하지 않는다.

 

 

 

반응형