Cuando varios hilos necesitan leer y escribir el mismo dato, los canales no siempre son la solución adecuada. Para esos casos, Rust proporciona Mutex<T> y RwLock<T>: tipos que garantizan acceso exclusivo al dato protegido. El compilador y el sistema de tipos aseguran que nunca accedas al dato sin adquirir el lock primero.
Mutex básico
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap(); // adquiere el lock
*num = 6;
} // MutexGuard sale del scope ? lock liberado automáticamente
println!("{:?}", m); // Mutex { data: 6 }
}
lock() bloquea hasta adquirir el lock y retorna un MutexGuard<T>. Cuando el guard sale de scope, el lock se libera. No hay posibilidad de olvidar liberarlo.
Arc + Mutex: el patrón estándar entre hilos
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&contador);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("Resultado: {}", *contador.lock().unwrap()); // 10
}
Envenenamiento del Mutex
Si un hilo hace panic mientras tiene el lock, el Mutex queda "envenenado". El siguiente hilo que intente adquirirlo obtendrá un Err de lock():
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let mutex = Arc::new(Mutex::new(0));
let m2 = Arc::clone(&mutex);
let _ = thread::spawn(move || {
let _guard = m2.lock().unwrap();
panic!("hilo con panic mientras tiene el lock");
}).join();
// El mutex está envenenado
match mutex.lock() {
Ok(v) => println!("OK: {}", v),
Err(e) => {
println!("Mutex envenenado, recuperando el valor");
let v = e.into_inner();
println!("Valor: {}", v);
}
}
}
RwLock: múltiples lectores o un escritor
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let datos = Arc::new(RwLock::new(vec![1, 2, 3]));
// Múltiples lectores simultáneos
let mut lectores = vec![];
for _ in 0..3 {
let d = Arc::clone(&datos);
lectores.push(thread::spawn(move || {
let leer = d.read().unwrap(); // múltiples lectores OK
println!("{:?}", *leer);
}));
}
for h in lectores { h.join().unwrap(); }
// Un único escritor
{
let mut escribir = datos.write().unwrap();
escribir.push(4);
}
println!("{:?}", datos.read().unwrap()); // [1, 2, 3, 4]
}
Mutex vs RwLock
| Aspecto | Mutex | RwLock |
|---|---|---|
| Lectores simultáneos | No (exclusivo) | Sí |
| Escritores simultáneos | No | No |
| Overhead | Bajo | Algo mayor |
| Cuándo elegir | Si escrituras son frecuentes o datos pequeños | Si lecturas son mucho más frecuentes que escrituras |
Cuándo usar canales en lugar de Mutex
Los canales son preferibles cuando:
- Los hilos trabajan de forma independiente y solo comparten resultados.
- Quieres un modelo productor-consumidor claro.
- Prefieres transferir la propiedad del dato en lugar de compartirlo.
El Mutex es preferible cuando:
- Múltiples hilos necesitan acceder al mismo dato con lecturas y escrituras entrelazadas.
- El estado compartido es demasiado complejo para modelarlo como mensajes.
Resumen
Mutex::lock(): acceso exclusivo, bloquea hasta obtener el lock.MutexGuard: libera el lock al salir del scope.- Envenenamiento: si un hilo panicea con el lock, el siguiente obtiene
Err. RwLock: múltiples lectores simultáneos O un escritor exclusivo.- Combina siempre con
Arcpara compartir entre hilos.
El siguiente artículo explica los traits Send y Sync: los dos marcadores que controlan qué tipos pueden usarse entre hilos y por qué Rust detecta las carreras de datos en tiempo de compilación.
