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:
- Uma nova goroutine é criada e colocada na fila local de um P disponível.
- M, associado a P, pega uma goroutine da fila local e a executa.
- Se a fila local estiver vazia, M tenta roubar goroutines de outros P.
- 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.
- 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.
- 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.