Základy Goroutines
Goroutine
Ak požiadate Gopherov, aby hovorili o výhodách golangu, často sa objaví text o súbežnosti (Concurrency). Základom tohto obsahu je goroutine, ktorý je ľahký a jednoduchý na spracovanie. Nižšie uvádzam stručný prehľad.
Súbežnosť (Concurrency) vs. Paralelizmus (Parallelism)
Pred pochopením goroutine sa chcem venovať dvom často zamieňaným pojmom.
- Súbežnosť (Concurrency): Súbežnosť sa týka spracovania mnohých úloh naraz. Neznamená to nevyhnutne, že sa skutočne vykonávajú súčasne, ale ide o štrukturálny a logický koncept, pri ktorom sa viacero úloh rozdelí na menšie jednotky a striedavo sa vykonáva, takže pre používateľa to vyzerá, že sa spracúva viacero úloh naraz. Súbežnosť je možná aj na jednom jadre.
- Paralelizmus (Parallelism): Paralelizmus znamená „spracovanie viacerých úloh súčasne na viacerých jadrách“. Doslova ide o paralelné vykonávanie úloh a spúšťanie rôznych úloh súčasne.
Goroutine umožňuje jednoduchú implementáciu súbežnosti prostredníctvom Go runtime schedulera a prirodzene využíva aj paralelizmus pomocou nastavenia GOMAXPROCS
.
Multi-thread (viacvláknové spracovanie) v Jave, ktoré sa bežne používa a má vysokú mieru využitia, je typickým príkladom konceptu paralelizmu.
Prečo je goroutine dobrý?
Ľahký (lightweight)
Náklady na vytvorenie sú veľmi nízke v porovnaní s inými jazykmi. Vzniká tu otázka, prečo golang využíva málo zdrojov? Je to preto, že miesto vytvorenia je spravované interne v Go runtime. Dôvodom je, že ide o vyššie uvedené ľahké logické vlákno, ktoré je menšie ako jednotka OS vlákna, vyžaduje počiatočný zásobník s veľkosťou približne 2 KB a dynamicky sa mení s pridaním zásobníka podľa implementácie používateľa.
Správa na úrovni zásobníka umožňuje veľmi rýchle a lacné vytváranie a odstraňovanie, takže spracovanie je zvládnuteľné aj pri miliónoch spustených goroutine. Vďaka runtime scheduleru môže Goroutine minimalizovať zásah jadra OS.
Výkonný (performance)
Po prvé, ako už bolo vysvetlené, Goroutine má menší zásah jadra OS, čo znižuje náklady na kontextové prepínanie na úrovni používateľa (User-Level) v porovnaní s jednotkou OS vlákna, a tým umožňuje rýchle prepínanie úloh.
Okrem toho sa na správu prideľuje OS vláknam pomocou modelu M:N. Vytvorením poolu OS vlákien je možné spracovanie aj s menším počtom vlákien bez potreby mnohých vlákien. Napríklad, ak sa dostane do stavu čakania, ako je systémové volanie, Go runtime spustí iné goroutine na OS vlákne, čím OS vlákno nepretržite a efektívne využíva CPU, čo umožňuje rýchle spracovanie.
Vďaka tomu môže Golang dosiahnuť vyšší výkon v I/O operáciách v porovnaní s inými jazykmi.
Stručný (concise)
Veľkou výhodou je aj to, že funkciu, ktorá vyžaduje súbežnosť, možno ľahko spracovať pomocou jediného kľúčového slova go
.
Je potrebné použiť zložité zámky, ako sú Mutex
, Semaphore
, a pri použití zámkov sa nevyhnutne musí zvážiť stav DeadLock, čo si vyžaduje komplexné kroky už vo fáze návrhu pred vývojom.
Goroutine v súlade s filozofiou „nekomunikujte zdieľaním pamäte, ale zdieľajte pamäť komunikáciou“ odporúča prenos dát prostredníctvom Channel
(kanála) a SELECT
podporuje aj funkciu, ktorá v kombinácii s Channel umožňuje spracovanie od kanála, kde sú dáta pripravené. Okrem toho, pomocou sync.WaitGroup
je možné jednoducho čakať, kým sa všetky goroutine dokončia, čo uľahčuje správu pracovného toku. Vďaka týmto nástrojom je možné predchádzať problémom s dátovými pretekmi medzi vláknami a bezpečnejšie spracovávať súbežnosť.
Tiež, použitím kontextu (context) je možné riadiť životný cyklus, zrušenie, timeout, deadline a rozsah požiadaviek na úrovni používateľa (User-Level), čím sa zaisťuje určitá úroveň stability.
Paralelná práca Goroutine (GOMAXPROCS)
Hoci bola reč o výhodách súbežnosti goroutine, možno sa pýtate, či nepodporuje paralelizmus. Je to preto, že počet jadier CPU v súčasnosti presahuje dvojciferné číslo, na rozdiel od minulosti, a aj domáce PC majú značný počet jadier.
Goroutine však vykonáva aj paralelnú prácu, a to je GOMAXPROCS
.
Ak nie je nastavený GOMAXPROCS
, nastavuje sa rôzne v závislosti od verzie.
Pred 1.5: Predvolená hodnota je 1, a ak je potrebná hodnota väčšia ako 1, je nevyhnutné nastaviť ju spôsobom ako
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 až 1.24: Zmenilo sa na všetky dostupné logické jadrá. Odvtedy vývojár nemusí nastavenie meniť, pokiaľ to nie je nevyhnutné.
1.25: Ako jazyk známy v kontajnerovom prostredí kontroluje cGroup v systéme Linux a overuje
obmedzenie CPU
nastavené pre kontajner.Ak je teda počet logických jadier 10 a obmedzenie CPU je 5,
GOMAXPROCS
sa nastaví na nižšiu hodnotu, t.j. 5.
Úprava vo verzii 1.25 priniesla veľmi významnú zmenu. Konkrétne sa zvýšila využiteľnosť jazyka v kontajnerovom prostredí. Tým sa zabránilo zbytočnému vytváraniu vlákien a kontextovému prepínaniu, čím sa predišlo 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: začiatok\n", name) // Začiatok simulácie práce
14 time.Sleep(10 * time.Millisecond) // Oneskorenie pre simuláciu práce
15 fmt.Printf("Goroutine %d: koniec\n", name) // Koniec simulácie práce
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // Použiť iba 2 jadrá CPU
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("Čakám, kým sa všetky goroutine dokončia...")
29 wg.Wait()
30 fmt.Println("Všetky úlohy boli dokončené.")
31
32}
33
Scheduler Goroutine (Model M:N)
V súvislosti s predchádzajúcou časťou o prideľovaní a správe OS vláknam pomocou modelu M:N existuje podrobnejší model goroutine GMP.
- G (Goroutine): Najmenšia jednotka práce vykonávaná v Go.
- M (Machine): OS vlákno (miesto skutočnej práce).
- P (Processor): Logický proces spravovaný Go runtime.
P má navyše lokálny spúšťací rad (Local Run Queue) a slúži ako scheduler, ktorý prideľuje pridelené G k M. Zjednodušene, goroutine.
Proces fungovania GMP je nasledovný:
- Keď sa vytvorí G (Goroutine), pridelí sa do lokálneho spúšťacieho radu P (Processor).
- P (Processor) pridelí G (Goroutine) z lokálneho spúšťacieho radu k M (Machine).
- M (Machine) vráti stav G (Goroutine): block, complete, preempted.
- Work-Stealing (kradnutie práce): Ak sa lokálny spúšťací rad P vyprázdni, iné P skontroluje globálny rad. Ak tam nie je žiadne G (Goroutine), ukradne prácu iného lokálneho P (Processsor), aby zabezpečilo, že všetky M pracujú nepretržite.
- Spracovanie systémových volaní (Blocking): Ak dôjde k Blocku počas vykonávania G (Goroutine), M (Machine) prejde do stavu čakania. V takom prípade sa P (Processor) oddelí od zablokovaného M (Machine) a spojí sa s iným M (Machine), aby spustil ďalšie G (Goroutine). Týmto sa neplytvá CPU ani počas čakania pri I/O operáciách.
- Ak jedno G (Goroutine) dlho preemptuje (prednostne využíva), poskytne sa príležitosť na spustenie inému G (Goroutine).
GC (Garbage Collector) v Golang beží tiež nad Goroutine, čo umožňuje paralelné čistenie pamäte s minimálnym prerušením vykonávania aplikácie (STW), čím efektívne využíva systémové zdroje.
Na záver, Golang má jednu zo silných výhod jazyka, a okrem toho aj mnoho ďalších, takže by som bol rád, keby si mnohí vývojári užili Golang.
Ďakujem.