Mutex y RwLock en Rust: proteger datos compartidos entre hilos

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

AspectoMutexRwLock
Lectores simultáneosNo (exclusivo)
Escritores simultáneosNoNo
OverheadBajoAlgo mayor
Cuándo elegirSi escrituras son frecuentes o datos pequeñosSi 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 Arc para 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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP