Concurrencia: hilos, canales y async/await

Forja tiene tres modelos de concurrencia que se complementan:

1. CSP: Hilos y Canales

Crear un hilo

🧵 Sintaxis
variable h = hilo {
    // se ejecuta en std::thread (Thread Pool)
    retornar resultado
}

// Esperar a que termine y obtener el resultado
variable r = h.unir()

Los hilos en Forja se ejecutan en hilos reales del sistema operativo a través de std::thread::spawn. Cada hilo tiene su propia pila y estado.

Canales mpsc

📡 canales
variable e, r = canal()

hilo {
    e.enviar("hola")
}

variable msg = r.recibir()   // bloquea hasta recibir
variable msg2 = r.try_recibir() // no bloqueante, retorna nulo si vacío

Los canales son mpsc (multiple producer, single consumer): múltiples hilos pueden enviar (enviar), pero solo un receptor puede leer (recibir).

Select sobre múltiples canales

Usa seleccionar para multiplexar la recepción de múltiples canales:

📡 seleccionar
seleccionar {
    caso rx1 -> dato -> {
        escribir("Canal 1: " + dato)
    }
    caso rx2 -> dato -> {
        escribir("Canal 2: " + dato)
    }
    tiempo 1000 -> {
        escribir("Timeout")
    }
    otro -> {
        escribir("Sin datos")
    }
}

El bloque seleccionar prueba cada canal con try_recibir no bloqueante y ejecuta el primero que tenga datos disponibles. El brazo tiempo ejecuta un timeout, y otro es el caso default si ningún canal tiene datos.

2. Async/Await

Las funciones asincrónicas se ejecutan en un Thread Pool (pool de hilos reutilizables). await es azúcar sintáctico para .unir().

Declaración de función asincrónica

⚡ función asincrónica
// Declaración con retorno
funcion asincrona tarea_lenta() -> Texto {
    esperar(2)
    retornar "¡Listo!"
}

// Declaración con parámetros
funcion asincrona suma(a: Entero, b: Entero) -> Entero {
    retornar a + b
}

funcion main() {
    // Llamar a función async → retorna un Future/Hilo
    variable h = tarea_lenta()

    // await = h.unir() (syntactic sugar)
    variable resultado = await h
    escribir(resultado) // → ¡Listo!

    // También funciona con .unir()
    variable r2 = suma(3, 4).unir()
    escribir(r2) // → 7
}

Múltiples llamadas async

Puedes lanzar varias funciones async y esperar sus resultados:

⚡ múltiples async
funcion asincrona tarea(id: Entero) -> Texto {
    retornar "Tarea " + id + " completada"
}

funcion main() {
    // Lanzar todas las tareas (se ejecutan en paralelo)
    variable t1 = tarea(1)
    variable t2 = tarea(2)
    variable t3 = tarea(3)

    // Esperar resultados (en cualquier orden)
    escribir(t1.unir())
    escribir(t2.unir())
    escribir(t3.unir())
}

Nota: En el MVP actual, await es bloqueante (como unir()). El thread pool evita crear un hilo nuevo por cada llamada, reutilizando hilos del pool.

Diferencia entre hilos clásicos y async/await

Hilos clásicos (hilo)Async/Await (funcion asincrona)
Sintaxis de bloque: hilo { ... } Sintaxis de función: funcion asincrona fn()
Se lanza inmediatamente Se lanza al llamar la función
Siempre crea un nuevo hilo del SO Usa un Thread Pool (reutiliza hilos)
No tiene tipo de retorno explícito Tipo de retorno explícito (-> Tipo)
Útil para tareas independientes Útil para tareas que esperan IO

3. Bloque reactivo cuando

El bloque cuando permite observar cambios en variables. Si la condición es una variable simple, cuando entra en un bucle que detecta cambios (polling cada 50ms). Si es una expresión compleja, se comporta como un si simple:

👀 Observador
// Watch reactivo (variable simple)
variable contador = 0
cuando (contador) {
    escribir("Contador cambió")
}

// If simple (expresión compleja)
cuando (contador > 10) {
    escribir("Mayor a 10")
}

4. stdlib de concurrencia

La stdlib std/concurrencia proporciona patrones de alto nivel:

pool_trabajadores(n, procesar)

Crea un pool de N workers. Retorna [tx_tareas, rx_resultados].

🏭 Pool de Workers
importar "std/concurrencia"

variable pool = pool_trabajadores(3, duplicar)
variable e = pool[0]  // canal para enviar tareas
variable r = pool[1]  // canal para recibir resultados

paralelo_for(inicio, fin, num_workers, callback)

Ejecuta iteraciones en paralelo usando M workers.

mapa_paralelo(arr, num_workers, callback)

Aplica una función a cada elemento del arreglo en paralelo.

con_limite_tiempo(ms, callback)

Ejecuta una función con límite de tiempo. Retorna Ok(valor) o Error("Tiempo agotado").

reintentar(max_intentos, callback)

Reintenta una función hasta N veces. El callback debe retornar un Resultado.

5. Funciones async en la stdlib

La stdlib también incluye versiones async que usan hilos reales:

FunciónDescripción
mapa_async(arr, callback)Map con un hilo real por elemento
paralelo_for_async(inicio, fin, cb)Parallel for con hilos reales
pool_async(n, procesar)Pool con workers en hilos reales
con_timeout_async(ms, cb)Timeout con hilo y try_recibir

Keywords de concurrencia

KeywordDescripción
hiloLanza un hilo (despachado al Thread Pool)
canal()Crea un canal de comunicación mpsc
.enviar(valor)Envía un dato por el canal
.recibir()Recibe un dato del canal (bloqueante)
.try_recibir()Recibe no bloqueante (retorna nulo si vacío)
.unir()Espera a que el hilo/Future termine
funcion asincronaDeclara función asincrónica (ThreadSpawn)
await exprEspera un Future/Hilo (equivale a expr.unir())
esperar(ms)Pausa la ejecución en milisegundos
seleccionarSelect multiplexado sobre canales (no bloqueante)
casoBrazo de recepción dentro de seleccionar
tiempoTimeout en seleccionar
otroCaso default en seleccionar
cuandoBloque observer/reactivo (polling si variable simple)

Ejemplo completo

📝 Ejemplo integrado
// Combinación de hilos, canales y async/await
funcion asincrona productor(canal_emisor) -> Texto {
    canal_emisor.enviar("dato 1")
    canal_emisor.enviar("dato 2")
    retornar "productor terminó"
}

funcion main() {
    variable e, r = canal()

    // Lanzar productor en un hilo
    hilo {
        variable res = productor(e)
        escribir(res)
    }

    // Consumir datos del canal
    escribir(r.recibir())
    escribir(r.recibir())
}

main()