En Rust, la concurrencia real a nivel del sistema operativo se logra mediante std::thread. A diferencia de las tareas async (que son ligeras y cooperativas), los hilos de std::thread son gestionados por el sistema operativo y permiten ejecutar código en paralelo en múltiples núcleos de la CPU. Esto los hace más costosos en términos de memoria y tiempo de creación, pero es la forma de obtener un paralelismo verdadero.

Cuando utilizas thread::spawn, creas un nuevo hilo de ejecución que recibe un closure. Debido a que el tiempo de vida de un hilo es independiente del hilo que lo creó, Rust impone la restricción 'static al closure: no puede capturar referencias a variables locales que podrían desaparecer (causando un dangling pointer). Para solucionar esto, se utiliza la palabra clave move, que transfiere la propiedad (ownership) de las variables desde el entorno local hacia el nuevo hilo.

El método spawn devuelve un JoinHandle<T>, donde T es el tipo de dato devuelto por el closure. Es fundamental llamar al método .join() sobre este handle; de lo contrario, el hilo principal podría terminar antes de que el hilo hijo haya finalizado su ejecución. El método join() devuelve un Result. Si el hilo hijo termina con éxito, devuelve Ok(T). Si el hilo hijo sufre un panic, join() devolverá Err, permitiendo que el hilo principal decida cómo reaccionar en lugar de colapsar todo el programa inmediatamente. Para configuraciones avanzadas, como asignar un nombre al hilo para depuración o ajustar el tamaño de la pila (stack size), se utiliza thread::Builder.

use std::thread;
use std::time::Duration;

fn main() {
    // Datos que serán movidos al hilo hijo
    let datos = vec![1, 2, 3, 4, 5];

    // 1. Uso de spawn con 'move' para transferir la propiedad de 'datos'
    let handle = thread::spawn(move || {
        println!("Hilo hijo: Procesando datos {:?}", datos);
        thread::sleep(Duration::from_millis(100));
        "Resultado del hilo básico" // Este es el tipo T en JoinHandle<T>
    });

    // 2. Uso de thread::Builder para configurar un hilo con nombre y stack específico
    let handle_builder = thread::Builder::new()
        .name("worker-thread".into())
        .stack_size(32 * 1024) // 32 KB de stack
        .spawn(|| {
            let id = thread::current().id();
            let nombre = thread::current().name().unwrap_or("Desconocido");
            println!("Hilo personalizado - ID: {:?}, Nombre: {}", id, nombre);
            
            // Simulamos un trabajo
            thread::sleep(Duration::from_millis(50));
            format!("Finalizado por {}", nombre)
        })
        .expect("Error al spawnear hilo con builder");

    // 3. Esperar a que los hilos terminen usando join()
    // El hilo principal se bloquea aquí hasta que el hilo hijo termine.
    match handle.join() {
        Ok(res) => println!("Éxito en hilo 1: {}", res),
        Err(_) => println!("El hilo 1 sufrió un panic"),
    }

    match handle_builder.join() {
        Ok(res) => println!("Éxito en hilo 2: {}", res),
        Err(_) => println!("El hilo 2 sufrió un panic"),
    }

    println!("Hilo principal finalizado.");
}

Explicación del Código

  • let datos = vec![...]: Definimos un vector en el hilo principal. Para poder usarlo dentro de un hilo que tiene un tiempo de vida 'static, debemos moverlo.
  • thread::spawn(move || { ... }): El uso de move captura la variable datos por valor, trasladando su propiedad al hilo hijo. Sin move, el compilador daría error porque datos no puede vivir lo suficiente para garantizar la seguridad de memoria.
  • thread::sleep(Duration::from_millis(100)): Pausa la ejecución del hilo actual por 100 milisegundos, simulando una carga de trabajo.
  • thread::Builder::new(): Crea un constructor para hilos con opciones personalizadas.
  • .name("worker-thread".into()): Asigna un nombre identificativo al hilo, útil al depurar procesos concurrentes.
  • .stack_size(32 * 1024): Establece un tamaño de pila manual de 32 KB para este hilo específico.
  • thread::current(): Retorna un objeto que representa el hilo que está ejecutando el código actual. Usamos .id() para obtener su identificador único y .name() para obtener su nombre asignado.
  • handle.join(): El método join() es llamado sobre los JoinHandle generados. Si el hilo hijo termina con éxito, devuelve Ok(T). En el primer caso, el valor devuelto es "Resultado del hilo básico".
  • expect(...) / match: Se utilizan para manejar los posibles errores. Si un hilo experimenta un panic, join devolverá un error que capturamos para evitar que el programa principal falle sin control.

80