Anatomía del Planificador G-M-P: Por Qué 100,000 Goroutines Consumen 200 MB Mientras 100,000 Hilos OS Colapsan (50 min)
Anatomía del Planificador G-M-P: Por Qué 100,000 Goroutines Consumen 200 MB Mientras 100,000 Hilos OS Colapsan
1. Hilos del Kernel (OS Threads) vs. Corrutinas Cooperativas/Preemptivas en Espacio de Usuario
Un hilo nativo del kernel Linux (pthread) reserva una pila fija de $1\text{ MB}$ a $8\text{ MB}$ con una página de guarda (guard page) y cada cambio de contexto requiere una interrupción de hardware que vacía registros SIMD y cuesta $\approx 1.5\text{ }\mu\text{s}$. En cambio, una Goroutine (G) inicia con una pila dinámica contigua de apenas $2\text{ KB}$ en el Heap y su cambio de contexto en espacio de usuario toma $\approx 150\text{ ns}$.
flowchart LR
G["G: Goroutine (Stack 2KB, PC, SP)"] --> P["P: Logical Processor (GOMAXPROCS RunQueue Local LRQ)"]
P --> M["M: OS Kernel Thread (Ejecuta Instrucciones en Core Físico)"]
P -.->|"Si LRQ vacía: Roba 50% de otro P"| WS["Work-Stealing / Global Run Queue (GRQ)"]
M -.->|"I/O Red Asíncrono"| NP["Netpoller (epoll / kqueue Sin Bloquear M)"]
2. Desacoplamiento Ante Syscalls Bloqueantes y Preempción Asíncrona (Go 1.14+)
1. Network I/O: Cuando una Goroutine lee un socket HTTP/PostgreSQL, el Netpoller intercepta el descriptor no-bloqueante y estaciona a G sin bloquear el hilo M, el cual toma inmediatamente la siguiente Goroutine de la cola de P.
2. Blocking OS Syscall (ej. lectura de disco local o CGO): M se desacopla de P (Handoff) para que otro hilo ejecute las Goroutines pendientes.
3. Preempción Basada en Señales (SIGURG): Evita que un bucle matemático cerrado sin llamadas a funciones congele un procesador lógico P por más de $10\text{ ms}$.