Rc y Arc en Rust: múltiple ownership con conteo de referencias

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::clone comparte propiedad en un hilo; el dato se libera cuando todos los clones salen de scope.
  • Arc es la versión thread-safe de Rc: usa contador atómico.
  • Para mutabilidad compartida entre hilos: Arc<Mutex<T>>.
  • Weak crea referencias que no incrementan el contador fuerte, evitando ciclos que causan fugas de memoria.
  • Nunca pases Rc a un hilo: el compilador lo rechaza porque no implementa Send.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP