Fearless concurrency: qué garantiza Rust y por qué es revolucionario
Rust garantiza la ausencia de carreras de datos en tiempo de compilacion: si el codigo seguro compila, no hay carrera de datos, punto. No garantiza la ausencia de carreras logicas ni de deadlocks. La misma regla de posesion y prestamo que da seguridad de memoria da seguridad entre hilos, con Send y Sync como puente. Por qué mover ese error de ejecucion a compilacion cambia la disciplina de un oficio entero.
“Concurrencia sin miedo” no es un eslogan de marketing: es una afirmación técnica precisa, y merece enunciarse con exactitud para no prometer de más ni de menos. Rust garantiza que el código seguro está libre de carreras de datos, y lo garantiza en tiempo de compilación: no hay detector que ejecutar, no hay suerte que tener en las pruebas, no hay heisenbug que solo aparece en producción un martes. Si compila, no hay carrera de datos. Esa frase, que suena modesta, invierte la historia entera de la programación concurrente, donde este error —el más temido de los sistemas— se combatía con disciplina frágil, detectores probabilistas o renuncias al rendimiento. Esta lección cierra el nivel diciendo con rigor qué garantiza Rust, qué no garantiza, cómo lo consigue con las reglas que ya dominas, y por qué eso constituye un cambio de época.
- Definir con precisión una carrera de datos y enunciar la garantía exacta de Rust seguro.
- Distinguir la carrera de datos (prevenida) de la carrera lógica y el deadlock (no prevenidos).
- Ver que la seguridad entre hilos emerge de posesión, préstamo y los marcadores
Send/Sync. - Situar por qué mover esta garantía a compilación es revolucionario frente a las alternativas históricas.
Qué garantiza exactamente: cero carreras de datos
Una carrera de datos tiene una definición técnica estricta: ocurre cuando dos o más hilos acceden a la misma posición de memoria a la vez, al menos uno escribe, y no hay sincronización que ordene esos accesos. Es comportamiento indefinido, igual que en C o C++: el compilador y el hardware pueden reordenar y romper cualquier suposición, produciendo valores imposibles, corrupción o fallos que desafían toda depuración.
La garantía de Rust es tajante y acotada: el código seguro no puede contener carreras de datos, y esto se verifica al compilar. No es una recomendación ni una tendencia estadística; es una propiedad del sistema de tipos. Los tres pilares que ya conoces bastan para sostenerla:
- Posesión: cada valor tiene un dueño; moverlo a un hilo transfiere ese dominio. Un dato poseído por un hilo no está aliasado por otro.
- Préstamo (alias XOR mutación): muchos
&To un único&mut T, nunca a la vez. Sin alias-más-mutación no hay carrera posible, por definición. SendySync: los marcadores que llevan esas reglas a través de la frontera del hilo, decidiendo qué tipos pueden migrar o compartirse sin romper sus invariantes.
La consecuencia práctica es que el sistema de tipos te encauza hacia la corrección: para tener estado mutable compartido entre hilos estás obligado a usar un tipo que aporte sincronización, porque solo esos son Sync mientras permiten mutar. El patrón canónico combina Arc (posesión compartida con contador atómico) y Mutex (mutación serializada):
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for _ in 0..4 {
let c = Arc::clone(&contador); // otro puntero al mismo Mutex
handles.push(thread::spawn(move || {
*c.lock().unwrap() += 1; // el cerrojo serializa el acceso
}));
}
for h in handles { h.join().unwrap(); }
println!("total: {}", *contador.lock().unwrap()); // siempre 4, sin carrera
}
Sustituye Arc<Mutex<u64>> por un simple u64 compartido por referencia y el programa deja de compilar: un &u64 no permite mutar, y ningún tipo de mutación compartida sin sincronizar es Sync. El compilador no te dicta el cerrojo, pero rechaza toda alternativa que no lo tenga, de modo que el único camino que compila es también el único correcto. Los detalles de Arc, Mutex y los tipos atómicos llegan en el nivel 30; aquí basta con reconocer la forma de la garantía.
Qué NO garantiza: carreras lógicas y deadlocks
La integridad de la afirmación depende de decir también lo que queda fuera. Rust previene la carrera de datos —un fenómeno a nivel de memoria—, no toda condición de carrera a nivel de lógica. Son cosas distintas:
- Una carrera lógica es cualquier resultado que dependa del entrelazado de los hilos: dos transferencias bancarias correctamente sincronizadas (cada acceso protegido por su
Mutex) pero aplicadas en un orden que deja un saldo inesperado. No hay carrera de datos —ningún acceso sin sincronizar— y sin embargo el resultado es un bug. Rust no lo impide, porque es un error de tu lógica, no de la memoria. - Un deadlock es igual de posible: dos hilos que toman dos
Mutexen orden opuesto pueden quedar bloqueados para siempre, cada uno esperando el cerrojo que el otro sostiene. El programa compila y es memory-safe; simplemente se cuelga.
Esta frontera no es una debilidad; es honestidad. Rust erradica la clase de fallos concurrentes que corrompen memoria y desafían la depuración, y deja en tus manos la corrección algorítmica, que ningún sistema de tipos razonable puede garantizar. “Sin miedo” no significa “sin pensar”.
Es la asimetría que define el alcance de la garantía. Bloquear dos Mutex en orden inconsistente y colgar el programa es código perfectamente válido para el compilador: no hay acceso sin sincronizar. En cambio, intentar mutar un dato compartido sin sincronización no compila, porque el tipo que lo permitiría no es Sync. Rust mueve a compilación el error que corrompe memoria y deja en ejecución el que “solo” cuelga o calcula mal. Conocer esa línea es saber qué te cubre el compilador y qué sigue siendo tu responsabilidad.
Por qué es revolucionario
Para medir la magnitud del cambio hay que recordar cómo se lograba la seguridad concurrente antes. Cada estrategia histórica pagaba un precio:
Disciplina y revisión
Convenciones y ojos humanos. Frágil: un solo descuido introduce una carrera que quizá no falle hasta producción, y de forma no reproducible.
Detectores en ejecución
ThreadSanitizer, el detector de Go. Solo ven las carreras que ocurren durante las pruebas; las que no se disparan ese día, se escapan.
Cerrojo global (GIL)
Python y Ruby serializan con un cerrojo global del intérprete: sin carreras, pero también sin paralelismo real de CPU.
Solo mensajes o inmutable
Erlang y los lenguajes funcionales prohíben el estado mutable compartido. Seguro, a costa de renunciar al modelo de memoria compartida.
Rust ofrece una quinta vía que no aparecía en el menú: garantía estática, con memoria compartida real, a coste cero en ejecución, y sin sacrificar el paralelismo. Puedes tener varios hilos mutando distintas partes de una estructura, o compartiendo estado a través de un Mutex, con la certeza —comprobada antes de ejecutar— de que ninguna combinación de entrelazados produce una carrera de datos. El término lo acuñó el equipo de Rust en 2015, y su sentido es psicológico además de técnico: puedes refactorizar y paralelizar sin miedo, porque el compilador rechaza el código incorrecto en lugar de castigarte con un fallo intermitente meses después.
flowchart TD O[Posesion cada valor un solo dueno] --> RULE[Alias o mutacion nunca ambas] B[Prestamo compartido o exclusivo] --> RULE SS[Send y Sync gobiernan el cruce de hilos] --> RULE RULE --> G[Cero carreras de datos en Rust seguro verificado en compilacion] G --> SI[Si el codigo seguro compila no hay carrera de datos] G --> NO[No cubre carreras logicas ni deadlocks] style RULE fill:#cba6f7,color:#11111b style G fill:#a6e3a1,color:#11111b style SI fill:#89b4fa,color:#11111b style NO fill:#fab387,color:#11111b
Lo revolucionario de la concurrencia sin miedo no es que Rust hiciera la concurrencia fácil —no lo hizo: los deadlocks siguen ahí, la corrección algorítmica sigue siendo tuya—, sino que reveló que la seguridad entre hilos no era un problema aparte, sino un corolario de la seguridad de memoria que Rust ya perseguía. Durante décadas la industria trató ambas como disciplinas separadas: una biblioteca de cerrojos por aquí, un recolector de basura por allá, un detector de carreras como herramienta externa. La intuición de Rust fue advertir que una carrera de datos es, desnuda de jerga, exactamente lo mismo que un uso-tras-liberación: acceso a un estado aliasado y mutable sin garantía de cuándo. La regla que Rust inventó contra el segundo —alias o mutación, jamás ambas— resultó ser, sin cambiar una coma, la cura del primero; solo hacía falta extenderla a través de la frontera del hilo, y para eso bastaron dos marcadores, Send y Sync, que el compilador deriva solo, propagando una verdad local sobre cada tipo hasta sus consecuencias globales. De ahí que la garantía sea estática y de coste cero: no hay un runtime vigilando en ejecución porque no hace falta vigilar lo que es imposible expresar. El resto de la industria había asumido, implícitamente, que la seguridad concurrente exigía elegir un sacrificio: renunciar al rendimiento con un cerrojo global, renunciar a la memoria compartida con actores puros, o renunciar a la certeza estática con detectores probabilistas y revisión humana. Rust demostró que ese trilema era falso, que existía una cuarta esquina donde no renuncias a nada de eso, y que el precio a pagar era distinto y anterior: aceptar un modelo de posesión y préstamo que te obliga a decir, en el tipo, quién puede tocar qué y cuándo. Pagado ese precio una vez —el que llevas todo el track pagando— la ausencia de carreras de datos cae como fruta madura, gratis. Ese es el sentido hondo de “sin miedo”: no la ausencia de dificultad, sino la ausencia de una categoría entera de terror, la del bug que no se reproduce, que aparece bajo carga, que se desvanece cuando añades un printf. Rust no lo detecta mejor: lo vuelve inexpresable en código seguro, y confina la posibilidad de escribirlo a los bloques unsafe, esa pequeña superficie auditada donde tú, y no el compilador, firmas la garantía. Mover un fallo desde “quizá lo cacemos en ejecución con suerte” hasta “no compila, nunca, jamás” no es una mejora incremental de la herramienta; es un cambio en la naturaleza del oficio.
Una carrera de datos es acceso concurrente a la misma memoria, al menos uno escritura, sin sincronización: comportamiento indefinido. Rust garantiza en compilación que el código seguro no tiene carreras de datos —si compila, no hay carrera—, mediante posesión, préstamo (alias XOR mutación) y los marcadores Send/Sync. No garantiza la ausencia de carreras lógicas ni de deadlocks: esos compilan y son problema tuyo. Es revolucionario porque las alternativas históricas pagaban con rendimiento (GIL), con el modelo (solo mensajes), o con la certeza (detectores en ejecución, disciplina); Rust da garantía estática, memoria compartida, coste cero y paralelismo a la vez. La seguridad entre hilos resultó ser el mismo teorema que la de memoria; unsafe es la superficie donde la garantía la firmas tú.
- Escribe la definición precisa de carrera de datos (tres condiciones) y explica por qué “alias o mutación, nunca ambas” la hace imposible sin comprobaciones en ejecución.
- Construye un ejemplo de carrera lógica con dos hilos que usan
Mutexcorrectamente pero producen un resultado dependiente del orden; argumenta por qué Rust no lo impide. - Esboza un deadlock con dos
Mutextomados en orden opuesto; confirma que compila y razona por qué queda fuera de la garantía. - Explica el papel de
SendySynccomo el puente que lleva la regla de préstamo a través de la frontera del hilo, y por qué son auto traits. - Compara la vía de Rust con el GIL de Python y con el modelo de actores de Erlang: qué sacrifica cada uno y qué evita pagar Rust a cambio de su modelo de posesión.