Índice

Async y event loop

El problema del bloqueo

En el Capítulo 17, aprendiste que tcp_accept bloquea — el programa se detiene y espera hasta que un cliente se conecta. En el Capítulo 18, resolviste esto con threads: crear un nuevo thread para cada cliente.

Pero los threads tienen costos. Cada thread usa memoria para su stack (~8KB mínimo). Con 10,000 conexiones concurrentes, son 80MB solo para stacks. Y cambiar entre threads tiene overhead.

I/O asíncrono es un enfoque diferente: en lugar de bloquear en cada operación, le dices al sistema operativo "avísame cuando haya datos listos" y continúas haciendo otro trabajo. Un solo thread puede manejar miles de conexiones.

Goroutines en Nyx

Nyx proporciona concurrencia ligera con spawn — similar a las goroutines de Go:

fn main() {
    spawn {
        print("Hola desde goroutine 1")
    }

    spawn {
        print("Hola desde goroutine 2")
    }

    sleep(50)    // esperar a que terminen las goroutines
    print("Main terminado")
}

A diferencia de thread_spawn (que crea un thread del sistema operativo), spawn crea una tarea ligera gestionada por el scheduler M:N de Nyx. Muchas goroutines se mapean sobre menos threads del SO.

El scheduler M:N

El scheduler de Nyx mapea N goroutines sobre M threads del SO:

Goroutines:   [g1] [g2] [g3] [g4] [g5] [g6] [g7] [g8]
                ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓
Threads SO:   [Thread 1]  [Thread 2]  [Thread 3]  [Thread 4]

El scheduler usa work-stealing: si un thread se queda sin goroutines, roba trabajo de la cola de otro thread. Esto mantiene todos los núcleos de CPU ocupados.

Select en channels

Cuando tienes múltiples channels, select te permite esperar al que tenga datos primero:

fn main() {
    let ch1: Map = channel_new(4)
    let ch2: Map = channel_new(4)

    spawn {
        sleep(100)
        channel_send(ch1, 42)
    }

    spawn {
        sleep(50)
        channel_send(ch2, 99)
    }

    select {
        case ch1 => {
            let v: int = channel_recv(ch1)
            print("ch1: " + int_to_string(v))
        }
        case ch2 => {
            let v: int = channel_recv(ch2)
            print("ch2: " + int_to_string(v))
        }
    }
}

select es como match para channels — elige el primer channel que tenga datos. Si ninguno está listo, bloquea hasta que uno lo esté.

El event loop

Internamente, Nyx usa un event loop basado en epoll (en Linux) para I/O asíncrono. Cuando una goroutine necesita esperar datos de red, en lugar de bloquear todo el thread del SO:

  1. Registra interés con epoll ("avísame cuando este socket tenga datos").
  2. Cede la goroutine, permitiendo que otras se ejecuten.
  3. Se despierta cuando llegan los datos.

Por esto http_serve_mt puede manejar más de 73,000 solicitudes por segundo — combina pools de threads con I/O dirigido por eventos.

Sintaxis async/await

Nyx también proporciona la sintaxis async/await:

async fn obtener_datos() -> String {
    return "datos cargados"
}

fn main() {
    let resultado: String = await obtener_datos()
    print(resultado)    // datos cargados
}

Nota importante: En la versión actual, async/await es azúcar sintáctico — no proporciona paralelismo adicional más allá de lo que spawn y threads ofrecen. La async fn crea un closure, y await lo llama. Para paralelismo real, usa spawn o thread_spawn.

Cuándo usar qué

Herramienta Mejor para Overhead
thread_spawn Trabajo intensivo de CPU ~8KB por thread + overhead del SO
spawn Goroutines I/O-bound ~1KB por goroutine
channel_new + workers Distribución de tareas Mínimo
http_serve_mt Servidores HTTP Combina threads + channels
select Esperar en múltiples channels Cero overhead

Regla general: Usa spawn para trabajo I/O-bound (red, archivos). Usa thread_spawn para trabajo CPU-bound (computación, compresión). Usa http_serve_mt para servidores HTTP.

Ejemplo práctico: fetcher concurrente

fn main() {
    let resultados: Map = channel_new(8)

    // Simular obtener datos de 4 fuentes concurrentemente
    spawn {
        sleep(100)    // simular latencia de red
        channel_send(resultados, 1)
    }

    spawn {
        sleep(200)
        channel_send(resultados, 2)
    }

    spawn {
        sleep(50)
        channel_send(resultados, 3)
    }

    spawn {
        sleep(150)
        channel_send(resultados, 4)
    }

    // Recolectar todos los resultados
    var i: int = 0
    while i < 4 {
        let r: int = channel_recv(resultados)
        print("Resultado recibido: " + int_to_string(r))
        i += 1
    }
    print("¡Todo listo!")
}

Los resultados llegan en orden de completitud (el más rápido primero), no en orden de creación.

Ejemplo práctico: patrón de timeout

fn main() {
    let resultado: Map = channel_new(1)
    let timeout: Map = channel_new(1)

    spawn {
        sleep(500)    // simular operación lenta
        channel_send(resultado, 42)
    }

    spawn {
        sleep(200)    // timeout después de 200ms
        channel_send(timeout, 0)
    }

    select {
        case resultado => {
            let v: int = channel_recv(resultado)
            print("Resultado obtenido: " + int_to_string(v))
        }
        case timeout => {
            channel_recv(timeout)
            print("¡Timeout!")
        }
    }
}

Si la operación tarda más de 200ms, el timeout se dispara primero.

Ejercicios

  1. Escribe un programa que cree 10 goroutines, cada una enviando su ID a través de un channel. Recolecta e imprime todos los IDs.
  1. Implementa un patrón fan-out/fan-in: un productor envía números del 1 al 100 a un channel, 4 workers elevan cada número al cuadrado, y un recolector suma los resultados.
  1. Usa select para implementar un sistema simple de prioridades: channels de alta y baja prioridad, donde el de alta prioridad siempre se verifica primero.
  1. Escribe una cuenta regresiva concurrente: crea 5 goroutines que cada una cuente de 5 a 1 con diferentes retrasos, todas imprimiendo su progreso.
  1. Implementa un wrapper de timeout: una función que ejecuta una computación en una goroutine y devuelve un valor por defecto si tarda demasiado.

Resumen

Siguiente capítulo: Sistemas — inline assembly, volatile, atomic →

← Anterior: Unsafe y punteros raw Siguiente: Sistemas — Assembly inline, volatile, atomic →