Los traits Send y Sync en Rust: qué tipos son seguros entre hilos

En la mayoría de los lenguajes, las carreras de datos (data races) solo se detectan en runtime con herramientas externas como ThreadSanitizer. En Rust, son un error de compilación. El mecanismo son dos traits marcadores: Send y Sync. Si un tipo no los implementa, el compilador rechaza el programa antes de ejecutarlo.

Send: mover un valor a otro hilo

Send significa que la propiedad del tipo puede transferirse a otro hilo. Casi todos los tipos de la stdlib implementan Send automáticamente.

use std::thread;

fn main() {
    let s = String::from("hola");
    let h = thread::spawn(move || {
        println!("{}", s); // s se movió al hilo: OK porque String: Send
    });
    h.join().unwrap();
}

Rc no es Send

use std::rc::Rc;
use std::thread;

fn main() {
    let rc = Rc::new(5);
    // thread::spawn(move || println!("{}", rc)); // ERROR de compilación
}
error[E0277]: `Rc<i32>` cannot be sent between threads safely
   |
   = help: the trait `Send` is not implemented for `Rc<i32>`

Rc usa un contador de referencias no atómico. Si dos hilos lo modificaran simultáneamente, el contador se corrompería. El compilador lo impide porque Rc no implementa Send. Usa Arc en su lugar.

Sync: compartir por referencia entre hilos

Sync significa que es seguro tener &T en múltiples hilos simultáneamente. Un tipo T es Sync si &T es Send.

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

fn main() {
    let datos = Arc::new(vec![1, 2, 3]); // Arc<Vec<i32>>: Send + Sync

    let d1 = Arc::clone(&datos);
    let d2 = Arc::clone(&datos);

    let h1 = thread::spawn(move || println!("{:?}", d1));
    let h2 = thread::spawn(move || println!("{:?}", d2));

    h1.join().unwrap();
    h2.join().unwrap();
}

RefCell no es Sync

use std::cell::RefCell;
use std::sync::Arc;
use std::thread;

fn main() {
    let celda = Arc::new(RefCell::new(0));
    let c = Arc::clone(&celda);
    // thread::spawn(move || { *c.borrow_mut() += 1; }); // ERROR
}
error[E0277]: `RefCell<i32>` cannot be shared between threads safely
   = help: the trait `Sync` is not implemented for `RefCell<i32>`

RefCell usa borrow checking en runtime no atómico: si dos hilos lo usaran a la vez, podrían corromperse las verificaciones. Usa Mutex<T> en su lugar.

Auto traits: se implementan automáticamente

Send y Sync son auto traits: el compilador los implementa automáticamente para cualquier tipo cuyos campos también los implementen. No necesitas escribir nada.

struct MiStruct {
    n: i32,         // i32: Send + Sync
    s: String,      // String: Send + Sync
}
// MiStruct: Send + Sync automáticamente

Si algún campo no es Send o Sync, el struct tampoco lo será.

Implementación manual (unsafe)

// Puedes implementar Send/Sync manualmente si sabes que es seguro
// pero requiere unsafe: estás asumiendo la responsabilidad tú
unsafe impl Send for MiTipo {}
unsafe impl Sync for MiTipo {}

Esto es necesario en código de bajo nivel que gestiona punteros raw. En código normal, nunca debería necesitarse.

Resumen de tipos y sus traits

TipoSendSyncUsar entre hilos
i32, String, Vec…Directamente
Arc<T> (T: Send+Sync)Directamente
Rc<T>NoNoNo posible
RefCell<T>NoSolo con Mutex
Mutex<T> (T: Send)Con Arc

Resumen

  • Send: la propiedad puede transferirse a otro hilo. Implementado automáticamente salvo para tipos como Rc.
  • Sync: la referencia puede compartirse entre hilos. Implementado automáticamente salvo para RefCell y similares.
  • Son auto traits: el compilador los deriva según los campos del tipo.
  • Las violaciones son errores de compilación, no bugs en runtime.

Los dos siguientes artículos cambian de perspectiva: comparan Rust con Go y con los lenguajes con excepciones, para ayudarte a decidir cuándo usar Rust en proyectos reales.

COMPARTE ESTE ARTÍCULO

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