Concurrencia en Rust: desde mutex hasta async/await
Cuando se habla de sistemas de alto rendimiento, Rust se sitúa instantáneamente en el centro de atención. Pero, ¿qué lo hace verdaderamente único? Es la concurrencia en Rust — un enfoque con el que obtienes la velocidad de C y la seguridad de la memoria sin sacrificar la legibilidad del código. En este artículo, analizaremos tres herramientas principales: los hilos clásicos, las ligeras gorutinas (en el contexto de Rust, tareas asíncronas) y el moderno async/await. ¡Allá vamos!
¿Por qué Rust no le teme a la concurrencia?
A diferencia de muchos lenguajes, Rust resuelve el problema de las condiciones de carrera a nivel del compilador. El sistema de propiedad (ownership) y préstamo (borrowing) garantiza que no puedas modificar accidentalmente datos desde dos hilos simultáneamente. Esto no es magia, es una verificación estática estricta. Por eso, la programación multihilo en Rust se considera una de las más seguras.
1. Hilos (std::thread)
Los hilos clásicos del sistema operativo son la base. En Rust se implementan a través del módulo std::thread. Cada hilo es un proceso ligero independiente que puede ejecutarse en paralelo en diferentes núcleos de la CPU.
Ejemplo: lanzar hilos
use std::thread;use std::time::Duration;
fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("Hilo: {}", i); thread::sleep(Duration::from_millis(1)); } });
for i in 1..5 { println!("Hilo principal: {}", i); thread::sleep(Duration::from_millis(1)); }
handle.join().unwrap();}Aquí, thread::spawn crea un nuevo hilo, y join espera a que termine. Sencillo, pero hay un detalle: los hilos son "pesados". Cada hilo consume al menos 2-8 MB de memoria de pila. Para miles de tareas simultáneas, esto no es adecuado.
¿Cuándo usar hilos?
- Cuando se necesita verdadero paralelismo (tareas intensivas en CPU).
- Cuando el número de hilos no supera el número de núcleos * 2.
- Cuando se trabaja con operaciones bloqueantes (lectura de archivos, llamadas síncronas).
2. Gorutinas en Rust: ¿mito o realidad?
Estrictamente hablando, Rust no tiene gorutinas en el mismo sentido que Go. Pero tiene tareas asíncronas (tasks) que se comportan casi igual: son ligeras, cambian de contexto de forma cooperativa (sin intervención del SO) y pueden ejecutarse por miles en un solo hilo. Para esto se utiliza el runtime Tokio o async-std.
Ejemplo: tarea asíncrona (gorutina) con Tokio
use tokio::task;
#[tokio::main]async fn main() { let handle = task::spawn(async { for i in 1..10 { println!("Gorutina: {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; } });
for i in 1..5 { println!("Principal: {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; }
handle.await.unwrap();}Observa: task::spawn crea una tarea ligera que se ejecuta dentro del pool de hilos de Tokio. El costo de memoria es de aproximadamente 2-3 KB por tarea. Esto permite ejecutar 100 000 "gorutinas" sin problemas. A diferencia de Go, aquí no hay un planificador integrado: tú eliges el runtime.
¿En qué se diferencian las gorutinas de Rust de las de Go?
- En Rust — las tareas son cooperativas: ellas mismas ceden el control mediante
.await. - En Go — las gorutinas son apropiativas: el planificador las interrumpe en puntos de E/S.
- Rust da más control: puedes elegir un runtime de un solo hilo o multihilo.
3. Async/await: el corazón del Rust asíncrono
Las palabras clave async y .await convierten el código síncrono en asíncrono sin