Concurrencia y multihilo en Rust: técnicas avanzadas

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Concurrencia y multihilo en Rust: nivel avanzado

\n\n

La 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\n

El material está dirigido a desarrolladores que ya conocen los fundamentos de Rust y desean profundizar en la computación paralela de alto rendimiento.

\n\n

1. Modelos de concurrencia: hilos del SO y asincronía

\n\n

Rust 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\n

Hilos del SO (std::thread)

\n\n

Crear un hilo en Rust es trivial, pero requiere atención a la propiedad de los datos:

\n\n
use std::thread;\n\nlet data = vec![1, 2, 3];\n\nlet handle = thread::spawn(move || {\n    println!("{:?}", data);\n});\n\nhandle.join().unwrap();
\n\n

El punto clave es la palabra clave move. Esta transfiere la propiedad al cierre. Si necesita compartir datos entre hilos, use Arc<Mutex<T>>.

\n\n

Programación asíncrona (async/await)

\n\n

El 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\n
use 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\n

La 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.

\n\n

2. Traits Send y Sync: seguridad en concurrencia

\n\n

Dos 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
\n\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.

\n\n

Veamos un ejemplo que no compilará debido a una violación de Send:

\n\n
use 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\n

El 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\n

3. Sincronización avanzada: canales, mutex y RwLock

\n\n

Para intercambiar datos entre hilos, Rust ofrece varias primitivas. Veamos las más potentes.

\n\n

Canales (mpsc)

\n\n

Los canales multihilo (multi-productor, único-consumidor) permiten enviar mensajes desde varios hilos a un solo receptor:

\n\n
use 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\n

Los canales son ideales para la arquitectura de "productor

Blogs

Book Recommendations