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

  1. Tworzy się nową gorutynę i umieszcza ją w lokalnej kolejce dostępnego P.
  2. M, powiązany z P, pobiera gorutynę z lokalnej kolejki i ją wykonuje.
  3. Jeśli lokalna kolejka jest pusta, M próbuje ukraść gorutiny od innych P.
  4. 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.
  5. 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.
  6. 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.