Concurrencia avanzada en Rust: Arc>, RwLock, atómicos y canales mpsc

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.

COMPARTE ESTE ARTÍCULO

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