El modelo de ownership de Rust garantiza un único propietario por valor. Pero hay escenarios donde varios lugares del código necesitan acceder al mismo dato: grafos, árboles con nodos hijos compartidos o caches en memoria. Para eso están Rc<T> (para uso en un solo hilo) y Arc<T> (para múltiples hilos), que implementan conteo de referencias.
Rc<T>: referencia contada en un hilo
Rc viene de Reference Counted. Cada vez que clonas un Rc, el contador interno sube en uno. Cuando el último Rc sale de scope, el contador llega a cero y el dato se libera. El coste es mÃnimo: dos incrementos/decrementos de un entero no atómico.
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hola"));
println!("Referencias: {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a); // solo clona el puntero, no el String
println!("Referencias: {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a);
println!("Referencias: {}", Rc::strong_count(&a)); // 3
} // c sale de scope ? contador baja a 2
println!("Referencias: {}", Rc::strong_count(&a)); // 2
println!("{} {}", a, b);
}
Nodos compartidos en un grafo
use std::rc::Rc;
#[derive(Debug)]
struct Nodo {
valor: i32,
hijos: Vec<Rc<Nodo>>,
}
fn main() {
let hoja = Rc::new(Nodo { valor: 10, hijos: vec![] });
// Dos nodos padre comparten el mismo hijo
let padre1 = Nodo {
valor: 1,
hijos: vec![Rc::clone(&hoja)],
};
let padre2 = Nodo {
valor: 2,
hijos: vec![Rc::clone(&hoja)],
};
println!("Hoja tiene {} referencias", Rc::strong_count(&hoja)); // 3
println!("padre1 hijo: {}", padre1.hijos[0].valor); // 10
println!("padre2 hijo: {}", padre2.hijos[0].valor); // 10
}
Arc<T>: Rc atómico para múltiples hilos
Arc es Atomically Reference Counted. Usa operaciones atómicas para el contador, lo que lo hace seguro entre hilos pero con un coste ligeramente mayor que Rc.
use std::sync::Arc;
use std::thread;
fn main() {
let datos = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let datos_clone = Arc::clone(&datos);
let h = thread::spawn(move || {
println!("Hilo {i}: suma = {}", datos_clone.iter().sum::<i32>());
});
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
}
Arc con Mutex para datos mutables
Arc por sà solo no permite mutabilidad. Si varios hilos necesitan modificar el dato, combÃnalo con Mutex:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&contador);
handles.push(thread::spawn(move || {
let mut guard = c.lock().unwrap();
*guard += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("Contador final: {}", *contador.lock().unwrap()); // 10
}
Weak<T>: romper ciclos de referencia
Si dos Rc se apuntan mutuamente, ninguno llegará a cero y habrá una fuga de memoria. Weak es una referencia que no incrementa el contador fuerte:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Nodo {
valor: i32,
padre: RefCell<Weak<Nodo>>,
hijos: RefCell<Vec<Rc<Nodo>>>,
}
fn main() {
let padre = Rc::new(Nodo {
valor: 1,
padre: RefCell::new(Weak::new()),
hijos: RefCell::new(vec![]),
});
let hijo = Rc::new(Nodo {
valor: 2,
padre: RefCell::new(Rc::downgrade(&padre)), // Weak, no sube contador
hijos: RefCell::new(vec![]),
});
padre.hijos.borrow_mut().push(Rc::clone(&hijo));
// Subir al padre desde el hijo
if let Some(p) = hijo.padre.borrow().upgrade() {
println!("Padre del hijo: {}", p.valor); // 1
}
println!("Rc padre fuertes: {}", Rc::strong_count(&padre)); // 1
println!("Rc hijo fuertes: {}", Rc::strong_count(&hijo)); // 2
}
El error si mezclas Rc con hilos
use std::rc::Rc;
use std::thread;
fn main() {
let a = Rc::new(5);
let b = Rc::clone(&a);
// ERROR: `Rc<i32>` cannot be sent between threads safely
thread::spawn(move || {
println!("{}", b);
});
}
// Rc no implementa Send. Usa Arc en su lugar.
Resumen
Rc::clonecomparte propiedad en un hilo; el dato se libera cuando todos los clones salen de scope.Arces la versión thread-safe deRc: usa contador atómico.- Para mutabilidad compartida entre hilos:
Arc<Mutex<T>>. Weakcrea referencias que no incrementan el contador fuerte, evitando ciclos que causan fugas de memoria.- Nunca pases
Rca un hilo: el compilador lo rechaza porque no implementaSend.
