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.