Gorutyny to lekkie wątki wykonawcze wbudowane w architekturę Go od pierwszych wersji w celu osiągnięcia efektywnej współbieżności. Historycznie idea lekkiego wątku powstała jako próba obejścia kosztowności wątków systemowych oraz z powodu wysokiego zapotrzebowania na skalowalne aplikacje serwerowe. Go został pierwotnie zaprojektowany jako język do systemów serwerowych i sieciowych, gdzie miliony zadań muszą być przetwarzane równolegle.
Problem: Współbieżność może szybko prowadzić do warunków wyścigu, zakleszczeń i wzrostu zużycia pamięci, jeśli nie kontroluje się cyklu życia gorutyn, nie uwzględnia planowania, a także nie zarządza ich zakończeniem.
Rozwiązanie: Gorutyny uruchamia się za pomocą słowa kluczowego go. Praca gorutyn jest planowana przez planistę Go, który używa modelu M:N (M wątków systemowych obsługuje N gorutyn języka Go). Do zarządzania cyklem życia stosuje się kanały, WaitGroup, kontekst i kontrolę zamykania kanałów.
Przykład kodu:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Pracownik %d rozpoczął\n", id) time.Sleep(time.Second) fmt.Printf("Pracownik %d zakończył\n", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Kluczowe cechy:
Czy, jeśli w main nie zaczekamy na gorutynę, zawsze zostanie ona wykonana?
Nie, zakończenie main sprawia, że proces kończy się niezależnie od stanu podrzędnych gorutyn i nie wszystkie zadania zostaną wykonane.
Czy uruchomienie go func(...) w pętli gwarantuje, że każda gorutyna otrzyma swoją własną wartość zmiennych pętli?
Nie, pojawia się problem uchwycenia zmiennej pętli, gorutyny mogą pracować z tą samą wartością slice’a/zmiennej. Należy używać kopiowania zmiennej, na przykład przekazując ją jako argument:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
Czy jedna gorutyna może zablokować planistę Go, uniemożliwiając wykonanie innych?
Tak, jeśli uruchamia nieskończoną lub bardzo intensywną pętlę bez punktów przełączania (na przykład, bez wywołań funkcji czasu lub yield), może utrzymywać wątek systemowy — chociaż jest to sprzeczne z ideologią Go o "kooperacyjnej wielozadaniowości". Na przykład, intensywna funkcja bez blokad:
func busy() { for { // Brak oczekiwań lub wywołań blokujących } }
W mikroserwisie okresowo uruchamiana jest gorutyna do odczytu z bazy danych, ale zapominają ją zakończyć przy anulowaniu żądania. W rezultacie pozostają "wiszące" gorutyny, które z czasem prowadzą do zużycia całej pamięci RAM.
Zalety:
Wady:
Używany jest kontekst do kontrolowania anulowania zadań, WaitGroup — do zarządzania zakończeniem wszystkich gorutyn przed zatrzymaniem aplikacji, a kanały — do poprawnego przekazywania danych między wykonawcami.
Zalety:
Wady: