Guía completa de concurrencia en Rust para desarrolladores

rust Guía completa de concurrencia en Rust para desarrolladores

Guía completa de concurrencia en Rust

Rust ofrece primitivas y garantías que hacen la concurrencia más segura por diseño. Esta guía te lleva desde los conceptos clave (ownership, Send/Sync) hasta patrones prácticos (threads, channels, Mutex, async con Tokio), con ejemplos completos, estructura de proyecto y consejos para evitar errores comunes.

Conceptos clave: por qué Rust cambia las reglas

  • Ownership y borrowing: impiden condiciones de carrera en tiempo de compilación cuando aplicas las reglas correctamente.
  • Send y Sync: traits automáticos que definen si un tipo puede moverse entre hilos (Send) o ser referenciado concurrentemente (&T es seguro) (Sync).
  • Tipos de sincronización: Mutex/RwLock, canales (mpsc), atomics; cada uno tiene un propósito y coste distinto.
  • Async vs hilos: async (futuros y runtime como Tokio) es cooperativo y eficiente en I/O; threads son mejores para trabajo paralelo de CPU pesado.

Quick checks: cómo saber si un tipo es Send/Sync

En tu crate puedes forzar verificaciones con:

fn assert_send() {}
fn assert_sync() {}

// usa: assert_send<MyType>();

Ejemplo 1 — Hilos básicos

Cuando mueves ownership al hilo, evita referencias inválidas usando move:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("thread got: {:?}", v);
    });
    handle.join().unwrap();
}

Por qué: move transfiere ownership de v al closure, evitando que el hilo enfrente referencias a memoria que ya no existe.

Ejemplo 2 — Canales (mpsc) para comunicar hilos

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        tx.send(42).unwrap();
    });

    println!("got {}", rx.recv().unwrap());
}

Por qué: canales permiten pasar valores entre hilos sin compartir memoria mutable. Son buenos para pipelines y tareas productores/consumidores.

Ejemplo 3 — Contador compartido con Arc+Mutex

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = c.lock().unwrap();
            *num += 1;
        }));
    }

    for h in handles { h.join().unwrap(); }
    println!("Counter: {}", *counter.lock().unwrap());
}

Por qué: Arc permite compartir ownership en múltiples hilos; Mutex garantiza acceso exclusivo. Evita pasar &mut entre hilos.

Ejemplo 4 — Async con Tokio (I/O eficiente)

Usa async cuando necesitas muchas tareas I/O-bound sin tantas threads:

// Cargo.toml: add tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("hello from task");
        5
    });

    let res = handle.await.unwrap();
    println!("res {}", res);
}

Por qué: Tokio usa un runtime que multiplexa muchas tareas ligeras (futuros) sobre un pool de hilos, reduciendo overhead para mucha I/O concurrida.

Estructura de ejemplo de proyecto

Si quieres experimentar, crea un repo con ejemplos aislados:

concurrency-guide/
  threads/
    Cargo.toml
    src/
      main.rs
  channels/
    Cargo.toml
    src/
      main.rs
  mutex/
    Cargo.toml
    src/
      main.rs
  async_tokio/
    Cargo.toml
    src/
      main.rs

Cada subcrato contiene el ejemplo completo mostrado arriba. Así puedes compilar y ejecutar aislado, y comparar comportamientos y rendimiento.

Errores comunes y cómo evitarlos

  • Compartir &mut entre hilos: el compilador lo bloquea, pero algunos intentos de workaround (unsafe) introducen data races. Evita unsafe salvo que entiendas invariantes.
  • Bloqueo largo dentro de Mutex: no hagas I/O o esperas largas manteniendo un lock; extrae la lógica fuera para minimizar contención.
  • Deadlocks: ordena la adquisición de varios locks de forma consistente o usa try_lock con retroceso y backoff.
  • Confusión async vs Send: algunos futures no son Send; cuando uses tokio::spawn asegúrate que el futuro implementa Send.

Buenas prácticas rápidas

  1. Prefiere mover ownership (Arc + datos inmutables) antes que compartir mutabilidad.
  2. Usa canales para comunicación y separación de responsabilidades.
  3. Minimiza la duración de los locks y prefiere RwLock si hay muchas lecturas y pocas escrituras.
  4. Para I/O intenso, usa async + runtime; para CPU-bound, usa threads y particiona trabajo.
  5. Incluye pruebas de estrés y herramientas de sanitizers (por ejemplo Miri o TSAN via wrappers) en etapas tempranas.

Herramientas y debugging

  • RUSTFLAGS="-Z sanitizer=thread" con nightly para detectar data races (experimental).
  • println! y logs son válidos, pero para tracing de concurrencia usa crates como tracing para correlación de spans.
  • Mira cargo bench y Criterion para comparar overhead de sincronización.

Patrones recomendados

  • Actor model con canales para sistemas con muchas entidades independientes.
  • Work-stealing y thread pools para carga CPU-bound (rayon para data-parallel).
  • Futuros Send + canales mpsc para combinar async con work dispatch.

Si necesitas, puedo generar los archivos Cargo.toml y main.rs listos para cada subproyecto del árbol de ejemplo arriba, o un ejemplo más avanzado combinando async y bloqueo mínimo (p. ej. un servidor pequeño con Tokio y workers). Sigue con lo que prefieras: generar código completo para probar localmente, recomendaciones de benchmarking o un checklist de seguridad para revisiones de código.

Consejo avanzado: para evitar deadlocks al adquirir múltiples Mutex, asigna una orden total a los recursos y adquiere locks siguiendo esa orden; si no es posible, usa try_lock y reintenta con backoff. Esto reduce la posibilidad de ciclos de bloqueo y es más fácil de razonar en revisiones de código.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión