rust-mutex-estado-compartido

Estado compartido en Rust con Mutex y Arc<Mutex<T>>

  • 4 min

Un Mutex es un mecanismo que serializa el acceso a un dato entre varios hilos.

Mutex es la abreviatura de Mutual Exclusion (Exclusión Mutua). Imagina que hay un solo micrófono en una sala de conferencias. Solo una persona puede hablar a la vez. Si alguien quiere hablar, debe esperar a que el actual orador suelte el micrófono.

En Rust, un Mutex<T> protege un dato T. Si un hilo quiere acceder al dato, debe pedir permiso (“bloquear” el Mutex).

La API de Mutex<T>

Mutex funciona de forma parecida a RefCell: ofrece Mutabilidad Interior. El dato dentro es inmutable, pero el Mutex nos permite obtener una referencia mutable de forma segura.

use std::sync::Mutex;

fn main() {
    // Creamos un Mutex que protege un entero
    let m = Mutex::new(5);

    {
        // 1. Pedimos el bloqueo (lock).
        // Esto BLOQUEA el hilo actual si alguien más lo está usando.
        // Devuelve un Resultado (unwrap es necesario por "lock poisoning", ver nota).
        let mut num = m.lock().unwrap();

        // 2. Modificamos el dato.
        // 'num' es un Smart Pointer (MutexGuard) que actúa como &mut i32.
        *num = 6;

    } // 3. Aquí 'num' sale del ámbito y el bloqueo se libera automáticamente.

    println!("m = {}", *m.lock().unwrap()); // m = 6
}
Copied!

Desbloqueo Automático En otros lenguajes (C++, Java, Go), debes recordar llamar a .unlock(). Si olvidas hacerlo (o si hay una excepción antes), creas un Deadlock eterno. En Rust, el desbloqueo está ligado al Trait Drop del MutexGuard. En cuanto la variable num muere, el candado se abre. Seguridad por defecto.

Compartir un mutex entre hilos

Ahora intentemos usar esto con concurrencia real. Queremos 10 hilos que incrementen un contador.

Si intentamos pasar el Mutex directamente, no podremos, porque el sistema de Ownership no nos deja dar el mismo Mutex a 10 hilos (el primero se lo llevaría con move).

Necesitamos Múltiples Dueños. ¿Recuerdas Rc<T>? Vimos que servía para esto, pero que no era seguro para hilos. Para concurrencia, debemos usar su hermano atómico: Arc<T>.

El patrón Arc<Mutex<T>>

Esta es la combinación estándar en Rust para estado compartido mutable:

  • Arc: Permite que múltiples hilos posean el dato.
  • Mutex: Asegura que solo uno lo modifique a la vez.
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 1. Creamos el contador protegido (Arc envuelve al Mutex)
    let contador = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        // 2. Clonamos el Arc para este hilo (barato y atómico)
        let contador_clon = Arc::clone(&contador);

        let handle = thread::spawn(move || {
            // 3. Bloqueamos el Mutex para acceder al número
            let mut num = contador_clon.lock().unwrap();
            *num += 1;
        });

        handles.push(handle);
    }

    // 4. Esperamos a todos los hilos
    for handle in handles {
        handle.join().unwrap();
    }

    // 5. Leemos el resultado final
    println!("Resultado: {}", *contador.lock().unwrap()); // 10
}
Copied!

¡Funciona! 10 hilos han incrementado el mismo número sin condiciones de carrera. Rust nos ha obligado a usar lock(), evitando que modifiquemos la memoria a lo loco.

Los traits Send y Sync

En el artículo anterior vimos el trait Send (seguro para enviar a otro hilo). Existe otro trait llamado Sync.

  • Send: El tipo T se puede enviar (mover) a otro hilo.
  • Sync: El tipo T se puede compartir entre hilos mediante referencias (&T).

La gracia de Mutex es que puede ofrecer acceso sincronizado aunque T no sea Sync por sí mismo. En concreto, Mutex<T> es Sync cuando T: Send, porque el valor protegido puede ser transferido de forma segura entre los hilos que adquieren el bloqueo.

Interbloqueos (deadlocks)

Aunque Rust nos protege de corromper la memoria (Race Conditions), NO nos protege de la lógica incorrecta. Aún es posible causar un Deadlock.

Un Deadlock ocurre si:

  1. Hilo A bloquea el recurso 1 y espera por el 2.
  2. Hilo B bloquea el recurso 2 y espera por el 1.
  3. Ambos se quedan esperando eternamente.
let bloqueo1 = m1.lock().unwrap();
// ... operación lenta ...
let bloqueo2 = m2.lock().unwrap(); // Si otro hilo hizo esto al revés -> Deadlock
Copied!

Rust no detecta esto al compilar. Es responsabilidad tuya diseñar la lógica para evitar ciclos de espera.