GoSuda

Základy GoRoutines

By hamori
views ...

Goroutine

Pokud požádáte Gophery, aby hovořili o výhodách golangu, často se objeví články související s konkurentností (Concurrency). Základem tohoto obsahu je goroutine, která umožňuje lehké a jednoduché zpracování. Níže je stručné pojednání o tomto tématu.

Konkurentnost (Concurrency) vs. Paralelismus (Parallelism)

Před pochopením goroutine bych rád nejdříve objasnil dva pojmy, které se často zaměňují.

  • Konkurentnost: Konkurentnost se týká zpracování mnoha úkolů najednou. Neznamená to nutně, že se úkoly skutečně spouštějí současně, ale jedná se o strukturální a logický koncept, kdy se více úkolů rozdělí na menší jednotky a střídavě se spouštějí, takže se uživateli jeví, že se zpracovává více úkolů současně. Konkurentnost je možná i na jednojádrovém procesoru.
  • Paralelismus: Paralelismus znamená „současné zpracování více úkolů na více jádrech“. Doslova jde o paralelní provádění práce a spouštění různých úkolů současně.

Goroutine umožňuje snadnou implementaci konkurentnosti prostřednictvím plánovače Go runtime a přirozeně využívá i paralelismus pomocí nastavení GOMAXPROCS.

Multi-threading v Javě, která je často používána a má vysokou míru využití, je typickým příkladem konceptu paralelismu.

Proč je Goroutine tak dobrá?

Lehkost (lightweight)

Náklady na vytvoření jsou ve srovnání s jinými jazyky velmi nízké. Zde vyvstává otázka, proč golang spotřebovává méně zdrojů? Důvodem je, že správa místa vytvoření probíhá interně v Go runtime. Je to proto, že se jedná o výše zmíněné lehké logické vlákno; je menší než jednotka OS vlákna, vyžaduje počáteční zásobník o velikosti přibližně 2 KB a může se dynamicky měnit přidáním zásobníku podle implementace uživatele.

Díky správě v jednotkách zásobníku je vytváření a odstraňování velmi rychlé a levné, což umožňuje zpracování milionů goroutin bez zatížení. Díky tomu může Goroutine minimalizovat zásah jádra OS díky plánovači runtime.

Výkon (performance)

Za prvé, jak bylo vysvětleno výše, Goroutine má malý zásah jádra OS, a proto je při přepínání kontextu na uživatelské úrovni (User-Level) levnější než jednotka OS vlákna, což umožňuje rychlé přepínání úkolů.

Kromě toho je spravována alokací na OS vlákna pomocí modelu M:N. Vytvořením fondu OS vláken je možné zpracování s menším počtem vláken, aniž by bylo potřeba mnoho vláken. Například, když se goroutine dostane do stavu čekání, jako je systémové volání, Go runtime spustí jinou goroutine na OS vlákně, čímž OS vlákno nezahálí a efektivně využívá CPU, což umožňuje rychlé zpracování.

Díky tomu může Golang dosahovat vyššího výkonu než jiné jazyky, zejména při I/O operacích.

Stručnost (concise)

Velkou výhodou je také to, že v případě potřeby konkurentnosti lze funkci snadno zpracovat pomocí jediného klíčového slova go.

Je nutné používat složité zámky, jako jsou Mutex a Semaphore, a pokud se zámky používají, je nevyhnutelné zvážit stav DeadLock, což vyžaduje složité fáze již ve fázi návrhu před vývojem.

Goroutine se řídí filozofií „Nekomunikujte sdílením paměti, ale sdílejte paměť komunikací“ a doporučuje přenos dat prostřednictvím Channel (kanálu). SELECT ve spojení s Channel podporuje i funkci, která umožňuje zpracování dat z kanálu, který je připraven. Kromě toho lze snadno spravovat pracovní tok pomocí sync.WaitGroup, který umožňuje jednoduše čekat, dokud se všechny goroutiny nedokončí. Díky těmto nástrojům lze předcházet problémům se souběhem dat mezi vlákny a bezpečněji zpracovávat konkurentnost.

Dále je možné použít kontext (context) k řízení životního cyklu, zrušení, timeoutu, deadlinu a rozsahu požadavku na uživatelské úrovni (User-Level), což zajišťuje určitou úroveň stability.

Paralelní práce Goroutine (GOMAXPROCS)

Mluvili jsme o výhodách konkurentnosti goroutine, ale možná vás napadne, zda nepodporuje i paralelismus. Počet jader CPU v současné době přesahuje dvouciferná čísla, na rozdíl od minulosti, a ani domácí PC nemají malý počet jader.

Goroutine však provádí i paralelní práci, a to pomocí GOMAXPROCS.

Pokud GOMAXPROCS není nastaveno, nastavuje se odlišně podle verze.

  1. Před verzí 1.5: Výchozí hodnota 1, pokud je potřeba více než 1, je nutné nastavení, například pomocí runtime.GOMAXPOCS(runtime.NumCPU()).

  2. Verze 1.5 až 1.24: Změněno na počet všech dostupných logických jader. Od této doby není potřeba, aby vývojář prováděl nastavení, pokud to není nutné.

  3. Verze 1.25: Jako jazyk známý v kontejnerovém prostředí kontroluje cGroup v systému linux a ověřuje omezení CPU nastavené pro kontejner.

    Pokud je počet logických jader 10 a hodnota omezení CPU je 5, GOMAXPROCS se nastaví na nižší číslo, tedy 5.

Úprava ve verzi 1.25 představuje velmi významnou změnu. Zvýšila se totiž využitelnost jazyka v kontejnerovém prostředí. To umožnilo zabránit zbytečnému vytváření vláken a přepínání kontextu, čímž se zamezilo 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) // 작업 시뮬레이션을 위한 지연 // Zpoždění pro simulaci práce
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Start
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Používat pouze 2 CPU jádra
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이 끝날 때까지 대기합니다...") // Čekání, dokud se všechny goroutiny nedokončí...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // Všechny úkoly jsou dokončeny.
31
32}
33

Plánovač Goroutine (model M:N)

V části předchozího obsahu, kde se uvádí, že je správa prováděna alokací na OS vlákna pomocí modelu M:N, se podíváme podrobněji na model goroutine GMP.

  • G (Goroutine): Nejmenší jednotka práce spouštěná v Go
  • M (Machine): OS vlákno (skutečné místo provádění práce)
  • P (Processor): Logický proces spravovaný Go runtime

P navíc obsahuje lokální frontu spouštění (Local Run Queue) a funguje jako plánovač, který přiděluje G alokované na M. Zjednodušeně řečeno, goroutine

Průběh modelu GMP je následující:

  1. Když se vytvoří G (Goroutine), je přidělena do lokální fronty spouštění P (Processor).
  2. P (Processor) přidělí G (Goroutine) z lokální fronty spouštění M (Machine).
  3. M (Machine) vrátí stav G (Goroutine), který je buď block, complete, nebo preempted.
  4. Work-Stealing (krádež práce): Pokud se lokální fronta spouštění P vyprázdní, jiné P zkontroluje globální frontu. Pokud ani tam není G (Goroutine), ukradne práci jinému lokálnímu P (Processor), aby zajistilo, že všechna M neustále pracují.
  5. Zpracování systémového volání (Blocking): Pokud dojde k Block během spouštění G (Goroutine), M (Machine) přejde do stavu čekání. V takovém případě se P (Processor) oddělí od zablokovaného M (Machine) a spojí se s jiným M (Machine), aby spustilo další G (Goroutine). Tímto způsobem nedochází k plýtvání CPU ani během čekací doby I/O operací.
  6. Pokud jedna G (Goroutine) monopolizuje (preempted) spouštění po dlouhou dobu, dá se příležitost ke spuštění jiné G (Goroutine).

GC (Garbage Collector) v Golangu se také spouští nad Goroutine, což umožňuje paralelní čištění paměti s minimálním přerušením spuštění aplikace (STW), čímž se efektivně využívají systémové zdroje.

Na závěr, Golang má jednu ze silných výhod jazyka, a je jich mnohem více, takže doufám, že si ho mnoho vývojářů užije.

Děkuji.