La concurrencia en Rust es segura por diseño: el sistema de ownership y el sistema de tipos impiden las carreras de datos en tiempo de compilación. Para los casos en que varios hilos necesitan acceder o modificar el mismo estado, la biblioteca estándar ofrece cuatro herramientas fundamentales: Arc<Mutex<T>>, RwLock, los tipos atómicos y los canales mpsc.
Arc<Mutex<T>>: estado compartido mutable entre hilos
Arc (Atomic Reference Counted) permite que múltiples hilos sean propietarios del mismo dato. Mutex garantiza que solo un hilo lo modifica a la vez. La combinación Arc<Mutex<T>> es el patrón estándar para estado compartido mutable.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (0..8).map(|_| {
let c = Arc::clone(&contador);
thread::spawn(move || {
for _ in 0..1000 {
let mut guard = c.lock().unwrap();
*guard += 1;
} // guard se libera aquà (Drop)
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("Contador final: {}", *contador.lock().unwrap()); // 8000
// Error tÃpico: Mutex poisoning
// Si un hilo hace panic mientras sostiene el lock, el Mutex queda envenenado.
// lock().unwrap() hace panic en el hilo siguiente.
// Para manejar esto sin propagar el panic:
let seguro = Arc::new(Mutex::new(0));
let resultado = std::panic::catch_unwind(|| {
let mut g = seguro.lock().unwrap();
*g = 99;
panic!("fallo intencional");
});
println!("Panic capturado: {}", resultado.is_err());
}
RwLock: lecturas concurrentes y escritura exclusiva
RwLock<T> permite múltiples lectores simultáneos o un escritor exclusivo. Es la herramienta adecuada cuando las lecturas son mucho más frecuentes que las escrituras, ya que evita bloquear lectores entre sÃ.
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
let config = Arc::new(RwLock::new(vec!["host=localhost", "port=8080"]));
// Múltiples lectores simultáneos
let lectores: Vec<_> = (0..4).map(|id| {
let cfg = Arc::clone(&config);
thread::spawn(move || {
let lectura = cfg.read().unwrap();
println!("Lector {id}: {} entradas", lectura.len());
})
}).collect();
for h in lectores {
h.join().unwrap();
}
// Escritura exclusiva
{
let mut escritura = config.write().unwrap();
escritura.push("timeout=30s");
println!("Config actualizada: {} entradas", escritura.len());
} // lock de escritura liberado aquÃ
// Comprobación final con lectura
println!("Puerto: {}", config.read().unwrap()[1]);
}
Tipos atómicos: contadores sin bloqueos
Para operaciones sencillas sobre enteros o booleanos, los tipos atómicos (AtomicU64, AtomicBool, etc.) son más eficientes que un Mutex porque operan sin bloqueos usando instrucciones hardware de compare-and-swap. No necesitan Arc<Mutex<_>>, solo Arc<AtomicT>.
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicBool, Ordering};
use std::thread;
fn main() {
let peticiones = Arc::new(AtomicU64::new(0));
let activo = Arc::new(AtomicBool::new(true));
let handles: Vec<_> = (0..4).map(|id| {
let req = Arc::clone(&peticiones);
let act = Arc::clone(&activo);
thread::spawn(move || {
while act.load(Ordering::Relaxed) {
req.fetch_add(1, Ordering::SeqCst);
if id == 2 {
// Simular trabajo y luego señalar parada
act.store(false, Ordering::SeqCst);
}
}
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("Peticiones procesadas: {}", peticiones.load(Ordering::SeqCst));
}
Canales mpsc: comunicación sin estado compartido
La alternativa al estado compartido es pasar mensajes. std::sync::mpsc (multiple producer, single consumer) proporciona canales de un solo sentido. Varios hilos pueden enviar mensajes por el mismo canal; solo uno puede recibirlos.
use std::sync::mpsc;
use std::thread;
#[derive(Debug)]
enum Tarea {
Procesar(String),
Terminar,
}
fn main() {
let (tx, rx) = mpsc::channel::<Tarea>();
// Cuatro productores
let handles: Vec<_> = (0..4).map(|id| {
let tx_clon = tx.clone();
thread::spawn(move || {
tx_clon.send(Tarea::Procesar(format!("hilo-{id}"))).unwrap();
})
}).collect();
// Señal de terminación
tx.send(Tarea::Terminar).unwrap();
drop(tx); // cierra el canal
// Un solo consumidor
for tarea in rx {
match tarea {
Tarea::Procesar(origen) => println!("Procesando: {origen}"),
Tarea::Terminar => { println!("Terminando"); break; }
}
}
for h in handles {
h.join().unwrap();
}
}
La regla de diseño en Rust: prefiere los canales (mpsc) cuando los hilos tienen roles distintos (productor/consumidor) y los datos fluyen en una dirección. Usa Arc<Mutex<T>> solo cuando varios hilos necesiten leer y escribir el mismo estado bidireccional. Para lecturas frecuentes y escrituras esporádicas, RwLock tiene mejor rendimiento que Mutex. Para contadores y flags, los tipos atómicos son siempre la opción más rápida.
