Concurrencia y multihilo en Rust: nivel avanzado
\n\nLa concurrencia y el multihilo son uno de los aspectos más potentes y, al mismo tiempo, más complejos de la programación de sistemas. Rust ofrece un enfoque único: garantías de seguridad de memoria en tiempo de compilación, eliminando las condiciones de carrera (data race) y los punteros colgantes. En este artículo, analizaremos técnicas avanzadas para trabajar con concurrencia en Rust, incluyendo async/await, la biblioteca Tokio, los traits Send y Sync, así como primitivas de sincronización de bajo nivel.
\n\nEl material está dirigido a desarrolladores que ya conocen los fundamentos de Rust y desean profundizar en la computación paralela de alto rendimiento.
\n\n1. Modelos de concurrencia: hilos del SO y asincronía
\n\nRust admite dos modelos principales: hilos del sistema operativo (OS threads) y tareas asíncronas (async tasks). La elección entre ellos depende del escenario: para tareas intensivas en CPU, los hilos son más adecuados; para tareas vinculadas a E/S (I/O-bound), lo es async.
\n\nHilos del SO (std::thread)
\n\nCrear un hilo en Rust es trivial, pero requiere atención a la propiedad de los datos:
\n\nuse std::thread;\n\nlet data = vec![1, 2, 3];\n\nlet handle = thread::spawn(move || {\n println!("{:?}", data);\n});\n\nhandle.join().unwrap();\n\nEl punto clave es la palabra clave move. Esta transfiere la propiedad al cierre. Si necesita compartir datos entre hilos, use Arc<Mutex<T>>.
Programación asíncrona (async/await)
\n\nEl código asíncrono se ejecuta dentro de un solo hilo, cambiando entre tareas mientras espera E/S. Esto reduce la sobrecarga del cambio de contexto. Ejemplo de una función asíncrona simple:
\n\nuse tokio::time::{sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n let task1 = async_task("Tarea 1");\n let task2 = async_task("Tarea 2");\n\n tokio::join!(task1, task2);\n}\n\nasync fn async_task(name: &str) {\n println!("{} iniciada", name);\n sleep(Duration::from_secs(1)).await;\n println!("{} completada", name);\n}\n\nLa biblioteca tokio es el estándar para Rust asíncrono. Proporciona un planificador multihilo (work-stealing scheduler) que distribuye eficientemente las tareas entre los núcleos de la CPU.
2. Traits Send y Sync: seguridad en concurrencia
\n\nDos traits clave garantizan la seguridad en concurrencia en Rust:
\n\n- \n
- Send: un tipo se puede transferir de forma segura entre hilos. \n
- Sync: se puede hacer referencia de forma segura a un tipo desde múltiples hilos simultáneamente. \n
La mayoría de las primitivas de Rust implementan automáticamente estos traits si sus componentes los implementan. Por ejemplo, Arc<Mutex<i32>> es Send y Sync, mientras que Rc<RefCell<i32>> no lo es.
Veamos un ejemplo que no compilará debido a una violación de Send:
\n\nuse std::rc::Rc;\nuse std::thread;\n\nlet data = Rc::new(5);\n\nthread::spawn(move || {\n println!("{}", data);\n}); // Error: Rc<i32> no implementa Send\n\nEl compilador de Rust mostrará un mensaje de error claro, previniendo condiciones de carrera en tiempo de compilación. Esta es una de las principales ventajas del lenguaje.
\n\n3. Sincronización avanzada: canales, mutex y RwLock
\n\nPara intercambiar datos entre hilos, Rust ofrece varias primitivas. Veamos las más potentes.
\n\nCanales (mpsc)
\n\nLos canales multihilo (multi-productor, único-consumidor) permiten enviar mensajes desde varios hilos a un solo receptor:
\n\nuse std::sync::mpsc;\nuse std::thread;\n\nlet (tx, rx) = mpsc::channel();\n\nfor i in 0..3 {\n let tx_clone = tx.clone();\n thread::spawn(move || {\n tx_clone.send(i).unwrap();\n });\n}\n\ndrop(tx); // Cerramos el transmisor\n\nfor received in rx {\n println!("Recibido: {}", received);\n}\n\nLos canales son ideales para la arquitectura de "productor