rust-smart-pointers-rc-arc

Rc y Arc en Rust: compartir propiedad con seguridad

  • 5 min

Rc<T> y Arc<T> son punteros inteligentes para compartir la propiedad de un mismo dato.

Rust normalmente exige que cada valor tenga un único dueño. Pero también nos da excepciones controladas para los casos donde varias partes del programa necesitan apuntar al mismo dato.

Imagina una televisión en el salón de una casa compartida.

  • Todos los compañeros de piso “poseen” la televisión.
  • Cualquiera puede encenderla y verla.
  • ¿Cuándo se tira la televisión a la basura? Solo cuando el último compañero de piso se muda y la casa queda vacía.

Este concepto se llama Reference Counting (Conteo de Referencias). Rust nos ofrece dos tipos para manejarlo: Rc<T> y Arc<T>.

Rc<T>: Reference Counted (Mono-hilo)

Rc<T> permite que múltiples partes de tu código posean el mismo dato en el Heap. Funciona manteniendo un contador interno:

Cuando creamos una nueva referencia al dato, el contador sube (+1).

Cuando una referencia sale del ámbito (drop), el contador baja (-1).

Cuando el contador llega a cero, el dato se borra.

Ejemplo: Listas compartidas

Imagina que queremos dos listas que comparten la misma cola final.

  • Lista a: 5 -> 20 -> Nil
  • Lista b: 3 -> 20 -> Nil

El nodo 20 pertenece tanto a a como a b. Con Box esto sería imposible, porque Box no permite compartir propiedad.

use std::rc::Rc;

enum Lista {
    Cons(i32, Rc<Lista>),
    Nil,
}

use crate::Lista::{Cons, Nil};

fn main() {
    // 1. Creamos la parte compartida (cola)
    // Envolvemos Nil en un Rc para empezar la cadena
    let cola = Rc::new(Cons(20, Rc::new(Nil)));
    println!("Contador tras crear cola: {}", Rc::strong_count(&cola)); // 1

    // 2. Creamos la lista 'a' que apunta a 'cola'
    // Rc::clone NO copia los datos. Solo incrementa el contador. Es muy rápido.
    let a = Cons(5, Rc::clone(&cola));
    println!("Contador tras crear a: {}", Rc::strong_count(&cola)); // 2

    {
        // 3. Creamos la lista 'b' que TAMBIÉN apunta a 'cola'
        let b = Cons(3, Rc::clone(&cola));
        println!("Contador tras crear b: {}", Rc::strong_count(&cola)); // 3
    } // Aquí 'b' muere. El contador baja a 2.

    println!("Contador al final de main: {}", Rc::strong_count(&cola)); // 2
} // Aquí muere 'a'. Contador baja a 1.
  // Aquí muere 'cola' (la variable original). Contador baja a 0 -> Se libera memoria.
Copied!

Rc::clone(&cola) vs cola.clone() Podríamos llamar a cola.clone(), pero en Rust es convención usar Rc::clone(&cola). ¿Por qué?

Porque clone() a veces implica una copia profunda (Deep Copy) que puede ser lenta. Rc::clone deja visualmente claro que solo estamos incrementando un contador (operación barata) y no copiando todos los datos.

Acceso compartido e inmutabilidad

Rc<T> ofrece normalmente acceso compartido de solo lectura. Si somos el único propietario, Rc::get_mut puede proporcionar acceso mutable; cuando hay varios propietarios, necesitamos otro mecanismo.

Si necesitas modificar datos dentro de un Rc, necesitarás combinarlo con Mutabilidad Interior (RefCell), que veremos en el próximo artículo.

Arc<T>: Atomic Reference Counted (Multi-hilo)

Si intentas usar Rc<T> para compartir datos entre varios hilos (Threads), el compilador te dará un error.

Rc no es seguro para hilos (!Send). ¿Por qué? Porque la operación de “sumar 1” al contador no es atómica. Si dos hilos intentan clonar el Rc en el nanosegundo exacto, el contador podría corromperse, provocando fugas de memoria o “double free”.

Para esto existe Arc<T> (Atomic Reference Counted).

Su conteo funciona como el de Rc, pero utiliza primitivas atómicas para modificar el contador. Esto permite compartir la propiedad entre hilos si el tipo contenido también cumple los requisitos Send y Sync; Arc no vuelve segura cualquier mutación interna por sí solo.

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

fn main() {
    // Creamos un dato compartido para hilos
    let datos = Arc::new(vec![1.0, 2.0, 3.0]);

    let mut handles = Vec::new();

    for _ in 0..3 {
        // Clonamos el puntero atómico para pasarlo al hilo
        let datos_clon = Arc::clone(&datos);

        handles.push(thread::spawn(move || {
            // Cada hilo tiene su propio puntero al mismo vector en el Heap
            println!("Leído: {:?}", datos_clon);
        }));
    }

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

¿Por qué no usar siempre Arc?

Si Arc es más seguro, ¿para qué existe Rc? Por Rendimiento.

Las operaciones atómicas suelen tener más coste que incrementar un contador ordinario.

  • Si la propiedad nunca cruza de un solo hilo, usa Rc.
  • Si necesitas compartir entre hilos, paga el pequeño coste y usa Arc.

Comparativa de punteros con propiedad

Hasta ahora tenemos tres formas de poseer datos en el Heap:

PunteroPropiedadHilosUso principal
Box<T>ÚnicaTransferible si T: SendDatos grandes, recursividad, mover propiedad.
Rc<T>CompartidaNO SeguroEstructuras de grafos, múltiples dueños en un hilo.
Arc<T>CompartidaCompartible si T lo permiteCompartir propiedad entre hilos.

Un ciclo formado solo por referencias fuertes de Rc o Arc mantiene el contador por encima de cero y puede filtrar memoria. Para enlaces que no deben expresar propiedad, como el puntero de un hijo a su padre, usamos Weak<T>.