Middle+
Opowiedz o planowaniu goroutine w Go.
sobes.tech AI
Odpowiedź od AI
Planer gorutyn w Go używa modelu M:N, gdzie M gorutyn jest mapowanych na N wątków systemu operacyjnego. Efektywnie przełącza gorutiny między wątkami, implementując wielozadaniowość bez preempcji z punktami przerwania (punktami preempcji).
Główne komponenty modelu GPM:
- G (Goroutine): Abstrakcja jednostki kodu wykonywalnego, lekki wątek zarządzany przez planera Go.
- P (Procesor): Logiczny procesor, reprezentujący kontekst lokalnej kolejki gorutyn i zasobów takich jak mutexy i planer. Liczba P domyślnie jest określana przez liczbę logicznych rdzeni procesora, ale może być zmieniona przez
GOMAXPROCS. - M (Maszyna/Wątek): Wątek systemu operacyjnego, który wykonuje kod. M jest powiązany z P i wykonuje gorutiny z lokalnej kolejki P lub globalnej kolejki.
Sposób działania:
- Tworzy się nową gorutynę i umieszcza ją w lokalnej kolejce dostępnego P.
- M, powiązany z P, pobiera gorutynę z lokalnej kolejki i ją wykonuje.
- Jeśli lokalna kolejka jest pusta, M próbuje ukraść gorutiny od innych P.
- Jeśli P zostanie zablokowane (np. podczas wywołania systemowego), powiązany z nim M odłącza się, a inny M łączy się z tym P. Zablokowany M może być ponownie użyty, gdy wywołanie systemowe się zakończy.
- Jeśli gorutyna zostanie zablokowana na kanale lub mutexie, jest umieszczana w kolejce oczekujących, a planer wybiera inną gorutynę do wykonania na tym samym M.
- Planer wykonuje okresowe kontrole, aby wyeliminować długo działające gorutiny, umożliwiając innym gorutiny uzyskanie czasu CPU.
Punkty preempcji występują w określonych miejscach:
- Wywołania funkcji/metod.
- Pętle zawierające wywołania funkcji.
- Przydział pamięci.
- Primitives synchronizacji (kanały, mutexy).
Przykład planowania:
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int) {
fmt.Printf("Goroutine %d zaczyna pracę\n", id)
time.Sleep(time.Second) // Symulacja pracy
fmt.Printf("Goroutine %d zakończyła pracę\n", id)
}
func main() {
runtime.GOMAXPROCS(2) // Ustawia liczbę logicznych procesorów
for i := 1; i <= 5; i++ {
go worker(i) // Tworzy gorutiny
}
time.Sleep(3 * time.Second) // Czeka na zakończenie gorutyn
fmt.Println("Główny gorutyn zakończony")
}
W tym przykładzie 5 gorutyn jest planowanych na 2 logiczne procesory. Planer Go będzie rozdzielał te gorutiny między dostępne wątki systemu operacyjnego, używając lokalnych i ewentualnie globalnych kolejek, a także mechanizmu "kradzieży" pracy (work stealing).
Zalety takiego podejścia:
- Skalowalność: Efektywne wykorzystanie systemów wielordzeniowych.
- Niskie koszty: Lekkie gorutiny w porównaniu do wątków systemu operacyjnego.
- Łatwość użycia: Wbudowane planowanie w języku sprawia, że programowanie równoległe jest bardziej dostępne.