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
| Tipo | Send | Sync | Usar entre hilos |
|---|---|---|---|
| i32, String, Vec | Sí | Sí | Directamente |
| Arc<T> (T: Send+Sync) | Sí | Sí | Directamente |
| Rc<T> | No | No | No posible |
| RefCell<T> | Sí | No | Solo con Mutex |
| Mutex<T> (T: Send) | Sí | Sí | Con Arc |
Resumen
Send: la propiedad puede transferirse a otro hilo. Implementado automáticamente salvo para tipos comoRc.Sync: la referencia puede compartirse entre hilos. Implementado automáticamente salvo paraRefCelly 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.
