Middle+
Cuente sobre la planificación de goroutines en Go.
sobes.tech AI
Respuesta de la IA
El planificador de goroutines en Go utiliza el modelo M:N, donde M goroutines se asignan a N hilos del sistema operativo. Cambia eficientemente las goroutines entre hilos, implementando multitarea sin preempción con puntos de interrupción (puntos de preempción).
Componentes principales del modelo GPM:
- G (Goroutine): Abstracción de la unidad de código en ejecución, hilo ligero gestionado por el planificador de Go.
- P (Processor): Procesador lógico, que representa un contexto de cola local de goroutines y recursos como mutexes y planificador. La cantidad de P por defecto se determina por el número de núcleos lógicos del procesador, pero puede modificarse mediante
GOMAXPROCS. - M (Machine/Hilo): Hilo del sistema operativo que ejecuta el código. M está asociado con P y ejecuta goroutines desde la cola local de P o la cola global.
Modo de funcionamiento:
- Se crea una nueva goroutine y se coloca en la cola local de un P disponible.
- M, asociado con P, toma una goroutine de la cola local y la ejecuta.
- Si la cola local está vacía, M intenta robar goroutines de otros P.
- Si P se bloquea (por ejemplo, al realizar una llamada al sistema), M asociado se desacopla, y otro M se asocia con ese P. M bloqueado puede reutilizarse cuando la llamada al sistema termina.
- Si una goroutine se bloquea en un canal o mutex, se pone en cola de espera, y el planificador selecciona otra goroutine para ejecutar en ese mismo M.
- El planificador realiza verificaciones periódicas para desalojar goroutines que llevan mucho tiempo en ejecución, permitiendo que otras goroutines obtengan tiempo de CPU.
Los puntos de preempción ocurren en lugares específicos:
- Llamadas a funciones/métodos.
- Bucles que contienen llamadas a funciones.
- Asignación de memoria.
- Primitivos de sincronización (canales, mutexes).
Ejemplo de planificación:
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int) {
fmt.Printf("Goroutine %d empieza a trabajar\n", id)
time.Sleep(time.Second) // Simulación de trabajo
fmt.Printf("Goroutine %d termina de trabajar\n", id)
}
func main() {
runtime.GOMAXPROCS(2) // Establece el número de procesadores lógicos
for i := 1; i <= 5; i++ {
go worker(i) // Crea goroutines
}
time.Sleep(3 * time.Second) // Espera a que terminen las goroutines
fmt.Println("Goroutine principal terminada")
}
En este ejemplo, 5 goroutines se planifican en 2 procesadores lógicos. El planificador de Go distribuirá estas goroutines entre los hilos del sistema operativo disponibles, usando colas locales y posiblemente globales, así como el mecanismo de "robo" de trabajo (work stealing).
Ventajas de este enfoque:
- Escalabilidad: Uso eficiente de sistemas multinúcleo.
- Bajo costo: Goroutines ligeras en comparación con hilos del sistema operativo.
- Facilidad de uso: La planificación integrada en el lenguaje hace que la programación paralela sea más accesible.