Rc e Arc: múltiplos owners
1. O Problema do Ownership Único
No modelo de ownership padrão do Rust, cada valor tem exatamente um dono. Quando o dono sai de escopo, o valor é dropado. Isso é ótimo para segurança de memória, mas limitante para certos cenários.
Considere um grafo onde múltiplos nós precisam referenciar um mesmo nó central, ou uma árvore onde um nó filho tem dois pais. Com ownership único, seria impossível representar essas estruturas sem cópias profundas ou lifetimes complexos.
Para resolver isso, Rust oferece Rc<T> (Reference Counting) e sua versão thread-safe Arc<T>. Ambos permitem que um valor tenha múltiplos donos através de contagem de referências (reference counting) — um contador no heap que rastreia quantas referências ativas existem para o valor.
2. Rc: Reference Counting Single-threaded
Rc<T> é um ponteiro inteligente que conta referências de forma não-atômica, adequado apenas para cenários single-threaded. Internamente, Rc<T> aloca o valor no heap junto com dois contadores: strong_count (referências fortes) e weak_count (referências fracas, que veremos depois).
A diferença crucial entre Rc::clone() e o clone() tradicional é que Rc::clone() apenas incrementa o contador de referências, sem copiar o valor subjacente. É uma operação barata.
use std::rc::Rc;
let valor = Rc::new(42);
let ref1 = Rc::clone(&valor); // Apenas incrementa contador
let ref2 = valor.clone(); // Equivalente, também barato
println!("Contagem: {}", Rc::strong_count(&valor)); // 3
// Acesso ao valor via Deref
println!("Valor: {}", *ref1);
println!("Valor: {}", ref2.as_ref());
3. Rc na Prática: Exemplos e Padrões
Árvore Binária com Múltiplos Pais
use std::rc::Rc;
#[derive(Debug)]
struct No {
valor: i32,
filhos: Vec<Rc<No>>,
}
fn main() {
let folha = Rc::new(No {
valor: 3,
filhos: vec![],
});
let no_a = Rc::new(No {
valor: 1,
filhos: vec![Rc::clone(&folha)],
});
let no_b = Rc::new(No {
valor: 2,
filhos: vec![Rc::clone(&folha)],
});
println!("no_a: {:?}", no_a);
println!("no_b: {:?}", no_b);
println!("Contagem da folha: {}", Rc::strong_count(&folha)); // 3
}
Evitando Ciclos com Weak
Ciclos de referência são perigosos porque criam vazamentos de memória — os valores nunca são dropados. Weak<T> é uma referência que não aumenta o strong_count, permitindo que o valor seja dropado mesmo quando referências fracas existem.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Pessoa {
nome: String,
pais: RefCell<Vec<Rc<Pessoa>>>,
filhos: RefCell<Vec<Weak<Pessoa>>>, // Referências fracas para evitar ciclo
}
fn main() {
let mae = Rc::new(Pessoa {
nome: "Maria".to_string(),
pais: RefCell::new(vec![]),
filhos: RefCell::new(vec![]),
});
let filho = Rc::new(Pessoa {
nome: "João".to_string(),
pais: RefCell::new(vec![Rc::clone(&mae)]),
filhos: RefCell::new(vec![]),
});
// Adiciona referência fraca do filho na mãe
mae.filhos.borrow_mut().push(Rc::downgrade(&filho));
println!("Mãe: {:?}", mae);
println!("Strong count do filho: {}", Rc::strong_count(&filho)); // 1
println!("Weak count do filho: {}", Rc::weak_count(&filho)); // 1
}
4. Arc: Reference Counting Thread-safe
Arc<T> (Atomic Reference Counting) usa operações atômicas para garantir segurança em ambientes multi-threaded. O custo é um overhead de desempenho comparado a Rc<T>.
use std::sync::Arc;
use std::thread;
fn main() {
let dados = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let dados_clone = Arc::clone(&dados);
handles.push(thread::spawn(move || {
println!("Thread {}: {:?}", i, dados_clone);
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Contagem final: {}", Arc::strong_count(&dados)); // 1 (thread principal)
}
5. Combinação com Mutex e RwLock para Mutabilidade
Tanto Rc quanto Arc só permitem acesso imutável ao valor subjacente. Para mutabilidade compartilhada, precisamos combiná-los com RefCell (single-threaded) ou Mutex/RwLock (multi-threaded).
Arc>: Mutabilidade Segura entre Threads
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let contador_clone = Arc::clone(&contador);
handles.push(thread::spawn(move || {
let mut num = contador_clone.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Contador final: {}", *contador.lock().unwrap()); // 10
}
Arc>: Leitura Concorrente com Escrita Exclusiva
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let cache = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Leitores concorrentes
for _ in 0..5 {
let cache_clone = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let leitura = cache_clone.read().unwrap();
println!("Leitura: {:?}", *leitura);
}));
}
// Escritor exclusivo
let cache_clone = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let mut escrita = cache_clone.write().unwrap();
escrita.push(4);
}));
for handle in handles {
handle.join().unwrap();
}
}
6. Comparação e Boas Práticas
| Característica | Rc |
Arc |
|---|---|---|
| Thread safety | Não | Sim |
| Performance | Alta | Média (operações atômicas) |
| Uso típico | Single-threaded | Multi-threaded |
| Combinação com mutabilidade | Rc |
Arc |
Quando usar cada um:
- Use Rc em código exclusivamente single-threaded para melhor performance
- Use Arc quando precisar compartilhar entre threads
- Prefira Box<T> ou empréstimos (&T, &mut T) quando possível — são mais simples e performáticos
Armadilhas comuns:
- Ciclos de referência: Use Weak<T> para quebrar ciclos
- Vazamentos de memória: Ciclos com Rc sem Weak nunca são dropados
- RefCell dentro de Rc: Pode causar panics em runtime se violar regras de empréstimo
- Clone desnecessários: Rc::clone() é barato, mas ainda tem custo atômico em Arc
Dica de performance: Em código single-threaded, Rc é significativamente mais rápido que Arc. Use Rc sempre que possível e reserve Arc apenas para cenários que realmente precisam de concorrência.
Referências
- The Rust Programming Language - Rc
, the Reference Counted Smart Pointer — Capítulo oficial do livro sobre Rccom exemplos práticos - Rust Reference - Rc Standard Library Documentation — Documentação completa da API de Rc
- Rust Reference - Arc Standard Library Documentation — Documentação completa da API de Arc
- Rust by Example - Rc and Arc — Tutoriais práticos com exemplos de Rc e Arc
- Rust Design Patterns - Reference Counting — Padrões de design envolvendo contagem de referências em Rust
- Rust Atomics and Locks - A Practical Guide — Livro que explica em detalhes o funcionamento de operações atômicas em Arc