Sobes.tech
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:

  1. Se crea una nueva goroutine y se coloca en la cola local de un P disponible.
  2. M, asociado con P, toma una goroutine de la cola local y la ejecuta.
  3. Si la cola local está vacía, M intenta robar goroutines de otros P.
  4. 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.
  5. 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.
  6. 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.