Základy Goroutines
Goroutine
Když požádáte Gophery, aby hovořili o výhodách jazyka golang, často se objevuje článek týkající se konkurence (Concurrency). Základem tohoto obsahu je goroutine, kterou lze zpracovat lehce a jednoduše. Napsal jsem o tom stručný přehled.
Konkurence (Concurrency) vs. Paralelismus (Parallelism)
Než porozumíme goroutine, rád bych nejprve objasnil dva pojmy, které se často zaměňují.
- Konkurence: Konkurence se týká zpracování mnoha úloh najednou. Neznamená to nutně, že se úlohy provádějí skutečně současně, ale jde o strukturální a logický koncept, který rozděluje více úloh na menší jednotky a střídavě je provádí, takže se uživateli zdá, že se více úloh zpracovává současně. Konkurence je možná i na jednojádrovém procesoru.
- Paralelismus: Paralelismus znamená „provádění více úloh současně na více jádrech“. Doslova jde o provádění úloh paralelně a souběžné spouštění různých úloh.
Goroutine usnadňuje implementaci konkurence prostřednictvím Go runtime scheduleru a přirozeně využívá paralelismus pomocí nastavení GOMAXPROCS.
Multi-threading v Javě, který je často vysoce využívaný, je typickým konceptem paralelismu.
Proč je Goroutine dobrá?
Lehká (lightweight)
Náklady na vytvoření jsou velmi nízké ve srovnání s jinými jazyky. Zde se nabízí otázka, proč golang používá méně prostředků? Je to proto, že místo vytvoření je spravováno uvnitř Go runtime. Důvodem je, že se jedná o výše zmíněné lehké logické vlákno, které je menší než jednotka OS vlákna, vyžaduje počáteční zásobník o velikosti přibližně 2KB a dynamicky se mění přidáváním zásobníku podle implementace uživatele.
Správa na úrovni zásobníku umožňuje velmi rychlé a levné vytváření a odstraňování, což umožňuje zpracování milionů goroutin bez zatížení. Díky tomu může Goroutine díky runtime scheduleru minimalizovat zásahy jádra operačního systému.
Vysoký výkon (performance)
Za prvé, Goroutine, jak je popsáno výše, vyžaduje méně zásahů jádra operačního systému, takže při přepínání kontextu na uživatelské úrovni jsou náklady nižší než u jednotky OS vlákna, což umožňuje rychlé přepínání úloh.
Kromě toho používá model M:N k přidělování a správě OS vláken. Vytvořením fondu OS vláken je možné zpracovávat úlohy s menším počtem vláken, aniž by bylo potřeba mnoho vláken. Například, když goroutine přejde do stavu čekání, jako je systémové volání, Go runtime spustí jinou goroutine na OS vlákně, čímž OS vlákno nepřestává pracovat a efektivně využívá CPU pro rychlé zpracování.
Díky tomu může Golang dosáhnout vyššího výkonu v I/O operacích ve srovnání s jinými jazyky.
Stručná (concise)
Velkou výhodou je také to, že v případě potřeby konkurence lze funkci snadno zpracovat jediným klíčovým slovem go.
Je nutné používat složité zámky jako Mutex a Semaphore, a při použití zámků je nevyhnutelné zvážit stav DeadLock, což vyžaduje složitou fázi již ve fázi návrhu před vývojem.
Goroutine doporučuje předávání dat prostřednictvím Channel podle filozofie „nesdílejte paměť komunikací, ale komunikujte sdílením paměti“, a SELECT ve spojení s Channel podporuje funkci zpracování dat z kanálu, který je připraven. Kromě toho, pomocí sync.WaitGroup lze snadno čekat, dokud se všechny goroutiny nedokončí, což usnadňuje správu pracovního toku. Díky těmto nástrojům lze zabránit problémům s datovými závody mezi vlákny a bezpečněji zpracovávat konkurenci.
Kromě toho, pomocí kontextu (context) lze na uživatelské úrovni řídit životní cyklus, zrušení, timeouty, deadliny a rozsah požadavků, což zajišťuje určitou úroveň stability.
Paralelní operace Goroutine (GOMAXPROCS)
Zmínil jsem dobré vlastnosti konkurence goroutine, ale možná se ptáte, zda nepodporuje paralelismus. Počet jader v moderních CPU je na rozdíl od minulosti dvouciferný a ani domácí PC nemají málo jader.
Goroutine však provádí i paralelní operace, a to je GOMAXPROCS.
Pokud GOMAXPROCS není nastaven, nastaví se odlišně v závislosti na verzi.
Před verzí 1.5: Výchozí hodnota 1, pokud je potřeba více než 1, je nutné nastavit pomocí
runtime.GOMAXPOCS(runtime.NumCPU()).Verze 1.5 ~ 1.24: Změněno na všechny dostupné logické jádra. Od této doby není potřeba, aby vývojáři nastavovali, pokud to není nutné.
Verze 1.25: Jako populární jazyk v kontejnerovém prostředí kontroluje cGroup v Linuxu a ověřuje
omezení CPUnastavené v kontejneru.Pokud je tedy počet logických jader 10 a omezení CPU je 5,
GOMAXPROCSse nastaví na nižší hodnotu 5.
Změna ve verzi 1.25 přináší velmi významné úpravy. Zvyšuje se totiž využitelnost jazyka v kontejnerovém prostředí. Díky tomu se snížilo zbytečné vytváření vláken a přepínání kontextu, čímž se zabránilo 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 task 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 completed.
31
32}
33
Plánovač Goroutine (model M:N)
V části o tom, jak se model M:N používá k přidělování a správě OS vláken, se podrobněji podíváme 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 přidělené G na M. Zjednodušeně řečeno, goroutine funguje takto:
Proces fungování GMP je následující:
- Když je G (Goroutine) vytvořena, je přidělena do lokální fronty spouštění P (Processor).
- P (Processor) přidělí G (Goroutine) z lokální fronty spouštění na M (Machine).
- M (Machine) vrátí stav G (Goroutine), který může být block, complete nebo preempted.
- 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 od jiného lokálního P (Processoru), aby se všechna M nepřetržitě pohybovala.
- Zpracování systémových volání (Blocking): Pokud během provádění G (Goroutine) dojde k Blocku, M (Machine) přejde do stavu čekání. V tomto okamžiku se P (Processor) oddělí od zablokovaného M (Machine) a spojí se s jiným M (Machine), aby spustil další G (Goroutine). Během čekání na I/O operace nedochází k plýtvání CPU.
- Pokud jedna G (Goroutine) zabírá procesor příliš dlouho (preempted), dá šanci provádění jiným G (Goroutine).
Go GC (Garbage Collector) také běží na Goroutine, což umožňuje paralelní úklid paměti s minimálním přerušením provádění aplikace (STW), čímž se efektivně využívají systémové zdroje.
A konečně, Golang je jednou z hlavních silných stránek jazyka, a je jich mnohem více, takže doufám, že si mnoho vývojářů Go užije.
Děkuji.