Cómo funciona el planificador de Go

Fuentes: The Scheduler | Internals for Interns

El sistema de planificación (scheduler) de Go es un componente crucial del runtime que gestiona la ejecución concurrente de goroutines, que son funciones ligeras que pueden ejecutarse simultáneamente. Dado que el número de goroutines puede superar con creces el número de núcleos de CPU disponibles, el scheduler se encarga de distribuir estas goroutines entre los hilos del sistema operativo (threads) de manera eficiente, evitando que ninguna goroutine se quede sin recursos.

El scheduler se basa en el modelo GMP, que involucra tres elementos clave: Goroutines (G), Máquinas (M) y Procesadores (P). Las Goroutines representan las unidades de trabajo concurrentes. Las Máquinas (M) son hilos del sistema operativo que ejecutan el código. Los Procesadores (P) son contextos de planificación abstractos que no son ni CPU ni hilos, sino que proporcionan un entorno para la ejecución eficiente de goroutines. Cada P tiene su propia cola de ejecución (run queue) y caché de memoria (mcache), lo que reduce la contención y mejora el rendimiento.

Cuando se crea una goroutine con go f(), el runtime crea una estructura G para rastrear su ejecución. Cada Máquina (M) tiene dos goroutines especiales: curg, que es la goroutine que se está ejecutando actualmente, y g0, que es una goroutine de propósito especial que se encarga de tareas de mantenimiento del runtime, como la planificación y la gestión de la memoria. El g0 es fundamental porque permite al scheduler cambiar rápidamente entre goroutines de usuario y tareas de mantenimiento sin bloquear el hilo del sistema operativo.

El número de Procesadores (P) está limitado por la variable de entorno GOMAXPROCS, que por defecto es igual al número de núcleos de CPU. Esto significa que, aunque puedas crear millones de goroutines, solo un número limitado de ellas pueden ejecutarse en paralelo en un momento dado. El scheduler se encarga de elegir qué goroutine ejecutar a continuación, basándose en factores como la disponibilidad de recursos y la prioridad.

El scheduler también mantiene un estado global (sched) que contiene información compartida entre todos los Procesadores y Máquinas, como colas de ejecución globales, listas de goroutines muertas y listas de Máquinas inactivos. Este estado global está protegido por un bloqueo, pero el acceso a él se minimiza para evitar cuellos de botella en el rendimiento.

En resumen, el scheduler de Go es un sistema complejo pero elegante que permite la ejecución concurrente eficiente de goroutines, maximizando la utilización de los recursos del sistema operativo y proporcionando una experiencia de programación concurrente sencilla y productiva.