¡Hola, colegas Go-geeks! 🚀
En este artículo vamos a destripar el pilar de la concurrencia en Go: las goroutines. Mucho se habla de lo fáciles que son de usar, pero… ¿qué sucede realmente cuando escribes go f()
? ¿Cómo gestiona Go miles (¡o millones!) de esas “ligeras” hebras sin petar el sistema operativo?
Veremos:
- El modelo M:N de goroutines vs. hilos OS.
- Las estructuras internas: G, M y P.
- El scheduler: work-stealing, parking/unparking.
- Costes reales y ejemplos de benchmark.
- Consejos prácticos y recomendaciones.
1. ¿Por qué goroutines y no hilos “a pelo”?
En lenguajes tradicionales (Java, C++), cada hilo OS consume decenas de kilobytes o más de stack, y el cambio de contexto es caro. Go introdujo en 2009 un modelo más ligero:
- Goroutine: hebra del lenguaje, con un stack inicial de apenas 2 KB (que crece y decrece dinámicamente).
- Hilo OS: recurso del sistema que maneja la ejecución real en CPU.
- Scheduler M:N: multiplexa M hilos OS para ejecutar N goroutines.
Esta capa intermedia permite lanzar miles de goroutines sin arruinar tu RAM o explotar el scheduler del kernel.
2. G, M y P: las tres siglas mágicas
El runtime de Go gira alrededor de tres estructuras clave:
Símbolo | Nombre | Rol principal |
---|---|---|
G | Goroutine | Contiene estado de la goroutine: stack, programa, args. |
M | Machine (hilo OS) | Representa un hilo del sistema operativo. |
P | Processor (contexto) | Guarda la “capacidad de ejecutar”: una cola de G listos. |
-
G (goroutine)
- Estructura
runtime.g
: puntero al PC, stack, estado (running, runnable, waiting…). - Stack dinámico: empieza en ~2 KB y se expande hasta varios MB si hay recursión profunda.
- Estructura
-
M (machine)
- Estructura
runtime.m
: mapea a un hilo real del SO. - Cada M puede ejecutar cero o una P a la vez.
- Estructura
-
P (processor)
- Estructura
runtime.p
: puente entre G y M. - Contiene una cola local de goroutines (
runqueue
) y acceso a la cola global.
- Estructura
Cuando arranca tu programa, Go crea GOMAXPROCS
Ps (por defecto igual al número de CPUs), y un pool inicial de Ms crece o desciende según la carga.
3. El corazón del scheduler M:N
3.1 Asignación de G a M vía P
- Una nueva goroutine se inserta en la cola local de la P que la lanzó.
- Si la P está siendo ejecutada por un M, éste saca una G de su cola y la ejecuta.
- Cuando la cola local se vacía, la P “roba” trabajo de otras Ps (work-stealing) o de la cola global.
3.2 Work-stealing
- Cada P mantiene un array circular de tamaño fijo (256) para su cola local.
- Si tu P no tiene Gs, revisa otra P (aleatoria o en ronda) para “robar” la mitad de su cola.
- Esto equilibra la carga sin necesidad de bloqueo global casi siempre.
3.3 Parking y unparking
Cuando una G se bloquea (por ejemplo, lectura de canal, bloqueo en syscall, sleep):
- Se marca como waiting y se mueve a la cola de espera adecuada.
- El M que la ejecutaba “desvincula” la P y busca otra P con trabajo o crea una nueva M si no hay Ps libres y estamos bajo el límite.
- Cuando el evento (IO, timer, canal) se dispara, la G se re-activa: va a la cola del P que la despierte (a veces la global).
4. Consejos prácticos y “gotchas” de rendimiento
Evita goroutines “huérfanas”: siempre acompáñalas de un contexto o WaitGroup, o acaban vivas consumiendo memoria.
No blockees tu P: llamadas de bloqueo largas (ej. IO sin usar el paquete net/http de Go) pueden consumir un M completo; mejor usa las primitivas del runtime o goroutines dedicadas.
Tweak de GOMAXPROCS: por defecto vale runtime.NumCPU(). Jugar con este valor puede mejorar throughput en programas IO-bound vs CPU-bound.
Perfilado: usa go test -bench y pprof para ver cuántas goroutines están vivas y analizar cuellos de botella.
Conclusión
Las goroutines no son magia negra, sino el resultado de un diseño cuidadoso en el runtime de Go.
Con un scheduler M:N, work-stealing y stacks dinámicos, Go te permite escribir código concurrente simple y eficiente sin renunciar al rendimiento.
Si te ha molado el viaje al interior del runtime, ¡dale ❤️, comenta tus dudas y comparte tus propios benchmarks! Y, por supuesto, sigue explorando: el recolector de basura, el modelo de memoria y el linker de Go tienen aún más secretos por descubrir.
Si prefieres un soporte visual, por aquí te dejo un video hablando de la concurrencia en Go:
¡Nos leemos en el próximo artículo! 👋
Top comments (0)