Sobes.tech
Middle+

Fale sobre o agendamento de goroutines em Go.

sobes.tech IA

Resposta da IA

O escalonador de goroutines em Go usa o modelo M:N, onde M goroutines são mapeadas para N threads do sistema operativo. Ele troca eficientemente as goroutines entre os threads, implementando multitarefa sem preempção com pontos de interrupção (pontos de preempção).

Componentes principais do modelo GPM:

  • G (Goroutine): Abstração da unidade de código em execução, thread leve gerido pelo escalonador de Go.
  • P (Processador): Processador lógico, representando um contexto de fila local de goroutines e recursos como mutexes e escalonador. A quantidade de P por defeito é determinada pelo número de núcleos lógicos do processador, mas pode ser alterada através de GOMAXPROCS.
  • M (Máquina/Thread): Thread do sistema operativo que executa o código. M está associado a P e executa goroutines a partir da fila local de P ou da fila global.

Modo de funcionamento:

  1. Uma nova goroutine é criada e colocada na fila local de um P disponível.
  2. M, associado a P, pega uma goroutine da fila local e a executa.
  3. Se a fila local estiver vazia, M tenta roubar goroutines de outros P.
  4. Se P ficar bloqueado (por exemplo, ao fazer uma chamada ao sistema), M associado se desliga, e outro M associa-se a esse P. M bloqueado pode ser reutilizado quando a chamada ao sistema termina.
  5. Se uma goroutine ficar bloqueada num canal ou mutex, ela é colocada na fila de espera, e o escalonador escolhe outra goroutine para executar nesse mesmo M.
  6. O escalonador realiza verificações periódicas para desalojar goroutines que estão a trabalhar há muito tempo, permitindo que outras goroutines obtenham tempo de CPU.

Os pontos de preempção ocorrem em locais específicos:

  • Chamadas de funções/métodos.
  • Laços que contêm chamadas de funções.
  • Atribuição de memória.
  • Primitivos de sincronização (canais, mutexes).

Exemplo de escalonamento:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker(id int) {
	fmt.Printf("Goroutine %d começa a trabalhar\n", id)
	time.Sleep(time.Second) // Simulação de trabalho
	fmt.Printf("Goroutine %d terminou de trabalhar\n", id)
}

func main() {
	runtime.GOMAXPROCS(2) // Define o número de processadores lógicos

	for i := 1; i <= 5; i++ {
		go worker(i) // Cria goroutines
	}

	time.Sleep(3 * time.Second) // Aguarda a conclusão das goroutines
	fmt.Println("Goroutine principal terminada")
}

Neste exemplo, 5 goroutines são planeadas em 2 processadores lógicos. O escalonador de Go distribuirá essas goroutines entre os threads do sistema operativo disponíveis, usando filas locais e possivelmente globais, bem como o mecanismo de "roubo" de trabalho (work stealing).

Vantagens desta abordagem:

  • Escalabilidade: Uso eficiente de sistemas multinúcleo.
  • Baixo overhead: Goroutines leves em comparação com threads do sistema operativo.
  • Facilidade de uso: O escalonador integrado na linguagem torna a programação paralela mais acessível.