Concurrencia y multihilo en Rust: escribiendo código paralelo seguro
Rust es uno de los pocos lenguajes que resuelve el problema de las condiciones de carrera (data races) en tiempo de compilación. Gracias al sistema de ownership y borrowing, el código concurrente en Rust se vuelve no solo rápido, sino también seguro. En este artículo, analizaremos los fundamentos del multihilo en Rust: creación de hilos, intercambio de datos a través de canales y mutex, y también conoceremos el modelo «Fearless Concurrency».
1. Fundamentos del multihilo: std::thread
Rust proporciona soporte integrado para hilos a través del módulo std::thread. Se puede crear un nuevo hilo usando la función thread::spawn, que acepta un closure.
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(); // esperamos a que el hilo termine}El método join() bloquea el hilo principal hasta que el hilo hijo finalice. Sin él, el programa podría terminar antes de que el hilo se ejecute.
2. Transferencia de datos entre hilos: canales (mpsc)
Para el intercambio de mensajes entre hilos en Rust se utiliza el modelo de canal multiproductor, un solo consumidor (mpsc — multiple producer, single consumer). Esto está implementado en el módulo std::sync::mpsc.
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
thread::spawn(move || { let val = String::from("¡Saludos desde el hilo!"); tx.send(val).unwrap(); // val ya ha sido movido, no se puede usar });
let received = rx.recv().unwrap(); println!("Recibido: {}", received);}Observe la palabra clave move en el closure: transfiere la propiedad de la variable tx al nuevo hilo. El método send() devuelve un Result, que puede ser Err si el receptor se ha cerrado.
Para crear múltiples productores, se puede clonar tx:
let (tx, rx) = mpsc::channel();let tx1 = tx.clone();
thread::spawn(move || { tx.send("Mensaje 1").unwrap();});
thread::spawn(move || { tx1.send("Mensaje 2").unwrap();});
for received in rx { println!("Recibido: {}", received);}3. Memoria compartida: Mutex y Arc
Los canales son convenientes para transferir datos, pero a veces es necesario compartir estado entre hilos. Para esto se utiliza Mutex (exclusión mutua) en combinación con Arc (contador de referencias atómico).
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Resultado: {}", *counter.lock().unwrap());}¿Por qué Arc y no Rc? El Rc normal no es thread-safe, ya que su contador de referencias no es atómico. Arc utiliza operaciones atómicas, lo que lo hace seguro para el multihilo.
Importante: Mutex bloquea el hilo al llamar a lock(). Al salir del ámbito, el bloqueo se libera automáticamente (gracias al trait Drop).