Rust Admin

Dominando Rc<T> e Arc<T> em Rust: Contagem de Referências em Projetos Reais Já leu

Rc : Contagem de Referências em Single-Thread (Reference Counting) é um tipo de dado que permite múltiplos proprietários para um mesmo valor em um programa single-thread. Diferente do sistema de ownership padrão do Rust, onde apenas um dono é permitido, mantém um contador interno que rastreia quantas referências imutáveis apontam para os dados. Quando o contador chega a zero, a memória é automaticamente liberada. O caso de uso clássico é quando você precisa compartilhar dados entre múltiplas partes do código sem transferir ownership. Considere uma estrutura de árvore onde múltiplos nós precisam apontar para o mesmo nó pai: é exatamente o cenário onde brilha. A desvantagem é que você só consegue referências imutáveis; se precisar modificar dados, deve combinar com . Por que não usar em Multi-thread? usa operações não-atômicas para incrementar/decrementar o contador. Em um ambiente multi-thread, múltiplas threads poderiam modificar o contador simultaneamente, causando race conditions. O compilador Rust impede isso: não implementa nem , então você simplesmente

Rc: Contagem de Referências em Single-Thread

Rc<T> (Reference Counting) é um tipo de dado que permite múltiplos proprietários para um mesmo valor em um programa single-thread. Diferente do sistema de ownership padrão do Rust, onde apenas um dono é permitido, Rc<T> mantém um contador interno que rastreia quantas referências imutáveis apontam para os dados. Quando o contador chega a zero, a memória é automaticamente liberada.

O caso de uso clássico é quando você precisa compartilhar dados entre múltiplas partes do código sem transferir ownership. Considere uma estrutura de árvore onde múltiplos nós precisam apontar para o mesmo nó pai: é exatamente o cenário onde Rc<T> brilha. A desvantagem é que você só consegue referências imutáveis; se precisar modificar dados, deve combinar com RefCell<T>.

use std::rc::Rc;

struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

impl Node {
    fn new(value: i32) -> Self {
        Node {
            value,
            children: Vec::new(),
        }
    }
}

fn main() {
    let shared_node = Rc::new(Node::new(42));

    // Clone cria uma nova referência, não copia os dados
    let ref1 = Rc::clone(&shared_node);
    let ref2 = Rc::clone(&shared_node);

    println!("Contador de referências: {}", Rc::strong_count(&shared_node));
    // Saída: 3 (shared_node + ref1 + ref2)

    drop(ref1);
    println!("Após drop: {}", Rc::strong_count(&shared_node));
    // Saída: 2
}

Por que não usar Rc<T> em Multi-thread?

Rc<T> usa operações não-atômicas para incrementar/decrementar o contador. Em um ambiente multi-thread, múltiplas threads poderiam modificar o contador simultaneamente, causando race conditions. O compilador Rust impede isso: Rc<T> não implementa Send nem Sync, então você simplesmente não consegue compartilhá-lo entre threads.

Arc: Atomic Reference Counting para Multi-thread

Arc<T> (Atomic Reference Counting) é a versão thread-safe de Rc<T>. Usa operações atômicas para gerenciar o contador, garantindo que múltiplas threads possam acessar o mesmo dado simultaneamente sem race conditions. A troca é um pequeno overhead de performance, compensado pela segurança.

Arc<T> é essencial quando você precisa compartilhar dados entre threads de forma segura. Um padrão comum é usá-lo com Mutex<T> para permitir modificações thread-safe. Sozinho, Arc<T> oferece apenas referências imutáveis, mas a combinação Arc<Mutex<T>> é poderosa: você consegue propriedade compartilhada com acesso exclusivo para modificação.

use std::sync::Arc;
use std::thread;

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

    for _ in 0..3 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            println!("Thread com contador: {:?}", counter_clone);
        });
        handles.push(handle);
    }

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

Arc com Mutex

Para modificar dados compartilhados entre threads, combine Arc<Mutex<T>>. O Mutex garante que apenas uma thread acessa os dados por vez, enquanto Arc permite que múltiplas threads possuam a referência.

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

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

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

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

    println!("Resultado: {}", *counter.lock().unwrap());
    // Saída: 5
}

Comparação Prática: Quando Usar Cada Um

Rc<T> é a escolha para programas single-thread onde você precisa de múltiplos donos. Sua implementação é mais eficiente porque não requer atomicidade. Use quando: estruturas de dados complexas (grafos, árvores), caching compartilhado ou componentes que precisam acessar um recurso comum sem transferência de ownership.

Arc<T> é obrigatório em multi-threading. O overhead de operações atômicas é negligenciável comparado aos benefícios de segurança. Use quando: compartilhar estado entre threads, implementar workers em thread pools ou quando dados precisam ser acessíveis de múltiplos lugares simultaneamente em execução paralela.

Aspecto Rc Arc
Single-thread
Multi-thread
Overhead Mínimo Atomicidade
Segurança em paralelo Nenhuma Total
Caso de uso Compartilhamento local Compartilhamento global
// Rc para single-thread: grafo de dependências
use std::rc::Rc;

struct Package {
    name: String,
    depends_on: Vec<Rc<Package>>,
}

// Arc para multi-thread: processamento distribuído
use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3, 4, 5]);
let handles: Vec<_> = (0..4).map(|i| {
    let d = Arc::clone(&data);
    thread::spawn(move || {
        println!("Thread {} vê: {:?}", i, d);
    })
}).collect();

Conclusão

Rc<T> e Arc<T> resolvem um dos maiores desafios em linguagens de baixo nível: compartilhar dados mantendo segurança de memória. Primeiro ponto: Rc<T> é para single-thread e usa contagem não-atômica; Arc<T> é para multi-thread com contagem atômica. Segundo ponto: ambos oferecem apenas referências imutáveis por padrão—combine com RefCell<T> ou Mutex<T> para mutabilidade. Terceiro ponto: escolha baseado em seu contexto (threads ou não) e o compilador Rust o guiará; ele impede misturar tipos inadequados em tempo de compilação.

Referências


Artigos relacionados