GoSuda

Podstawy Goroutines

By hamori
views ...

Goroutine

Jeśli poprosimy Gopherów o opowiedzenie o zaletach golang, często pojawia się tekst na temat konkurencyjności (Concurrency). Podstawą tej koncepcji jest lekki i łatwy w obsłudze mechanizm goroutine. Poniżej przedstawiam jego krótkie omówienie.

Konkurencyjność (Concurrency) vs Równoległość (Parallelism)

Zanim przejdziemy do zrozumienia goroutine, chciałbym najpierw wyjaśnić dwa często mylone pojęcia.

  • Konkurencyjność: Konkurencyjność dotyczy przetwarzania wielu zadań naraz. Nie musi to oznaczać, że są one faktycznie wykonywane jednocześnie; jest to raczej koncepcyjna i logiczna struktura, w której wiele zadań jest dzielonych na małe jednostki i wykonywanych na przemian, co sprawia wrażenie, że użytkownikowi wydaje się, iż wiele zadań jest przetwarzanych jednocześnie. Konkurencyjność jest możliwa nawet na pojedynczym rdzeniu.
  • Równoległość: Równoległość oznacza „jednoczesne przetwarzanie wielu zadań na wielu rdzeniach”. Oznacza to dosłownie prowadzenie prac równolegle i jednoczesne wykonywanie różnych zadań.

Goroutine, za pośrednictwem Go runtime scheduler, pozwala na łatwą implementację konkurencyjności, a dzięki ustawieniu GOMAXPROCS naturalnie wykorzystuje również równoległość.

Multithreading w Javie, często charakteryzujący się wysokim wykorzystaniem zasobów, jest typowym przykładem równoległości.

Dlaczego goroutine jest korzystne?

Lekkość (lightweight)

Koszt tworzenia jest bardzo niski w porównaniu do innych języków. Pojawia się pytanie, dlaczego golang zużywa ich tak niewiele? Dzieje się tak, ponieważ zarządzanie miejscem tworzenia odbywa się wewnętrznie w Go runtime. Wynika to z faktu, że jest to lekki wątek logiczny, mniejszy niż jednostka wątku OS, a jego początkowy stos wymaga około 2KB pamięci i jest dynamicznie zmienny, z możliwością dodawania stosu w zależności od implementacji użytkownika.

Zarządzanie na poziomie stosu umożliwia bardzo szybkie i tanie tworzenie i usuwanie, co pozwala na bezproblemowe przetwarzanie nawet milionów goroutine. Dzięki temu Goroutine, za sprawą runtime scheduler, może zminimalizować interwencję jądra OS.

Wydajność (performance)

Po pierwsze, jak wyjaśniono powyżej, Goroutine ma mniejszą interwencję jądra OS, co oznacza, że koszt przełączania kontekstu na poziomie użytkownika (User-Level) jest niższy niż w przypadku jednostek wątków OS, co pozwala na szybszą zmianę zadań.

Ponadto, wykorzystuje model M:N do zarządzania alokacją do wątków OS. Tworząc pulę wątków OS, możliwe jest przetwarzanie przy użyciu mniejszej liczby wątków, bez potrzeby posiadania wielu wątków. Na przykład, jeśli Goroutine wpadnie w stan oczekiwania, taki jak wywołanie systemowe, Go runtime uruchamia inne goroutine na wątku OS, co pozwala wątkowi OS na nieprzerwaną i efektywną eksploatację CPU, umożliwiając szybkie przetwarzanie.

Dzięki temu Golang może osiągać wyższą wydajność w zadaniach I/O w porównaniu do innych języków.

Zwięzłość (concise)

Dużą zaletą jest również to, że w przypadku potrzeby konkurencyjności, funkcja może być łatwo obsłużona za pomocą jednego słowa kluczowego go.

Wymagałoby to użycia skomplikowanych mechanizmów blokujących, takich jak Mutex i Semaphore, a wykorzystanie blokad nieuchronnie wymagałoby uwzględnienia stanu zakleszczenia (DeadLock), co wymagałoby skomplikowanych etapów już na etapie projektowania przed rozpoczęciem implementacji.

Goroutine, zgodnie z filozofią "Nie komunikuj się, dzieląc pamięć, lecz dziel pamięć, komunikując się", zaleca przesyłanie danych za pośrednictwem kanałów (Channel). SELECT wspiera również funkcjonalność, która w połączeniu z kanałami (Channel) umożliwia przetwarzanie danych z kanału, który jest gotowy jako pierwszy. Ponadto, użycie sync.WaitGroup pozwala na proste oczekiwanie na zakończenie wszystkich goroutine, co ułatwia zarządzanie przepływem pracy. Dzięki tym narzędziom możliwe jest zapobieganie problemom z wyścigiem danych między wątkami i bezpieczniejsze zarządzanie konkurencyjnością.

Ponadto, używając kontekstu (context), można kontrolować jego cykl życia, anulowanie, limit czasu (timeout), termin (deadline) oraz zakres żądania na poziomie użytkownika (User-Level), co zapewnia pewien stopień stabilności.

Równoległe przetwarzanie Goroutine (GOMAXPROCS)

Mimo że mówiliśmy o zaletach konkurencyjności goroutine, może pojawić się pytanie, czy nie wspiera ona równoległości. Liczba rdzeni CPU w dzisiejszych czasach przekracza dwie cyfry, a nawet domowe komputery PC posiadają znaczną liczbę rdzeni.

Jednak Goroutine wykonuje również zadania równoległe, a jest to możliwe dzięki GOMAXPROCS.

Jeśli GOMAXPROCS nie zostanie ustawione, jego wartość będzie się różnić w zależności od wersji.

  1. Przed 1.5: Wartość domyślna 1, jeśli potrzeba więcej niż 1, konieczne jest ustawienie w sposób, np. runtime.GOMAXPOCS(runtime.NumCPU()).

  2. 1.5 do 1.24: Zmieniono na wszystkie dostępne rdzenie logiczne. Od tego momentu programiści nie muszą go ustawiać, chyba że wymagają poważnych ograniczeń.

  3. 1.25: Jako język znany z użycia w środowiskach kontenerowych, sprawdza cGroup w systemie Linux i weryfikuje ograniczenie CPU ustawione dla kontenera.

    Jeśli liczba rdzeni logicznych wynosi 10, a limit CPU wynosi 5, GOMAXPROCS zostanie ustawione na niższą wartość 5.

Modyfikacja w wersji 1.25 jest bardzo istotną zmianą. Zwiększyła ona użyteczność języka w środowiskach kontenerowych. Dzięki temu możliwe jest uniknięcie niepotrzebnego tworzenia wątków i przełączania kontekstu, co zapobiega dławieniu CPU (throttling).

 1package main
 2
 3import (
 4	"fmt"
 5	"math/rand"
 6	"runtime"
 7	"time"
 8)
 9
10func exe(name int, wg *sync.WaitGroup) {
11	defer wg.Done()
12
13	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: start
14	time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // Delay for work simulation
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: start
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Use only 2 CPU cores
20	wg := sync.WaitGroup();
21  goroutineCount := 10
22	wg.Add(goroutineCount)
23
24	for i := 0; i < goroutineCount; i++ {
25		go exe(i, &wg)
26	}
27
28	fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Waiting for all goroutines to finish...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // All tasks are complete.
31
32}
33

Scheduler Goroutine (Model M:N)

Wracając do poprzedniego punktu, w którym stwierdzono, że goroutine wykorzystuje model M:N do zarządzania alokacją do wątków OS, istnieje bardziej szczegółowy model GMP goroutine.

  • G (Goroutine): Najmniejsza jednostka pracy wykonywana w Go
  • M (Machine): Wątek OS (rzeczywiste miejsce pracy)
  • P (Processor): Logiczny proces zarządzany przez Go runtime

P dodatkowo posiada lokalną kolejkę wykonania (Local Run Queue) i pełni rolę schedulera, który przydziela przydzielone G do M. W prostych słowach, goroutine

Proces działania GMP jest następujący:

  1. Po utworzeniu G (Goroutine), jest ono przydzielane do lokalnej kolejki wykonania P (Processor).
  2. P (Processor) przydziela G (Goroutine) znajdujące się w lokalnej kolejce wykonania do M (Machine).
  3. M (Machine) zwraca stan G (Goroutine): block, complete, preempted.
  4. Work-Stealing (kradzież pracy): Jeśli lokalna kolejka wykonania P stanie się pusta, inne P sprawdzają kolejkę globalną. Jeśli nie ma tam G (Goroutine), kradną pracę od innego lokalnego P (Processor), aby zapewnić, że wszystkie M działają bez przerwy.
  5. Obsługa wywołania systemowego (Blocking): Jeśli G (Goroutine) w trakcie wykonywania zostanie zablokowane (Block), M (Machine) przechodzi w stan oczekiwania. W tym momencie P (Processor) odłącza się od zablokowanego M (Machine) i łączy się z innym M (Machine), aby wykonać następne G (Goroutine). W ten sposób nie ma marnotrawstwa CPU nawet podczas czasu oczekiwania w trakcie operacji I/O.
  6. Jeśli jedno G (Goroutine) zbyt długo wyprzedza (preempted) inne, daje szansę wykonania innym G (Goroutine).

GC (Garbage Collector) w Golang również działa na goroutine, co pozwala na równoległe czyszczenie pamięci, minimalizując przerwy w działaniu aplikacji (STW), co efektywnie wykorzystuje zasoby systemowe.

Podsumowując, Golang ma wiele mocnych stron, a ta jest jedną z nich. Mam nadzieję, że wielu deweloperów będzie cieszyć się korzystaniem z Go.

Dziękuję.