GoSuda

Základy Goroutines

By hamori
views ...

Goroutine

Ak by ste mali požiadať Gopherov, aby vymenovali výhody golangu, často by sa objavil článok o súbežnosti (Concurrency). Základom tohto konceptu je goroutine, ktorá je ľahká a ľahko spracovateľná. V súvislosti s tým som pripravil stručný prehľad.

Súbežnosť (Concurrency) vs. Paralelizmus (Parallelism)

Predtým, než pochopíme goroutiny, rád by som objasnil dva často zamieňané pojmy.

  • Súbežnosť: Súbežnosť sa týka spracovania mnohých úloh naraz. Neznamená to, že sa úlohy skutočne vykonávajú súčasne, ale ide o štrukturálny a logický koncept, kde sa viacero úloh rozdelí na menšie jednotky a striedavo sa vykonávajú, čím sa pre používateľa javí, akoby sa spracovávali súčasne. Súbežnosť je možná aj na jednojadrovom procesore.
  • Paralelizmus: Paralelizmus znamená "súčasné spracovanie viacerých úloh na viacerých jadrách". Doslova ide o paralelné vykonávanie práce, pričom sa rôzne úlohy vykonávajú súčasne.

Goroutiny umožňujú jednoduchú implementáciu súbežnosti prostredníctvom Go runtime schedulera a prirodzene využívajú paralelizmus prostredníctvom nastavenia GOMAXPROCS.

Multi-thread Java, ktorá je často využívaná, je typickým príkladom konceptu paralelizmu.

Prečo sú Goroutiny dobré?

Ľahkosť (lightweight)

Náklady na ich vytvorenie sú v porovnaní s inými jazykmi veľmi nízke. Vzniká tu otázka, prečo golang používa menej zdrojov? Je to preto, že ich vytváranie je riadené vo vnútri Go runtime. Dôvodom je, že ide o odľahčené logické vlákna, ktoré sú menšie ako OS vlákna, vyžadujú počiatočný stack o veľkosti približne 2KB a dynamicky sa menia pridaním stacku podľa implementácie používateľa.

Vďaka riadeniu na úrovni stacku je vytváranie a odstraňovanie veľmi rýchle a lacné, čo umožňuje spracovať milióny goroutinov bez zaťaženia. Vďaka tomu môže Goroutine minimalizovať zásahy do jadra OS vďaka runtime scheduleru.

Vysoký výkon (performance)

Po prvé, Goroutine, ako je uvedené vyššie, má minimálne zásahy do jadra OS, čo znižuje náklady na prepínanie kontextu na úrovni používateľa (User-Level) v porovnaní s úrovňou OS vlákna, čím umožňuje rýchle prepínanie úloh.

Okrem toho sa spravujú prideľovaním k OS vláknam pomocou modelu M:N. Vytvorením OS thread poolu je možné spracovať úlohy s menším počtom vlákien, namiesto potreby mnohých vlákien. Napríklad, ak Goroutine prejde 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ť vysoký výkon, najmä pri I/O operáciách, v porovnaní s inými jazykmi.

Stručnosť (concise)

Veľkou výhodou je aj jednoduché spracovanie funkcií pomocou jedného kľúčového slova go, ak je potrebná súbežnosť.

Je potrebné použiť komplexné Locky ako Mutex, Semaphore a pri použití Lockov je nevyhnutné zohľadniť stav DeadLock, čo si vyžaduje komplexnú fázu už v štádiu návrhu pred vývojom.

Goroutine odporúča prenos dát prostredníctvom Channel v súlade s filozofiou "nekomunikujte zdieľaním pamäte, ale zdieľajte pamäť komunikáciou", a SELECT v kombinácii s Channel podporuje funkciu spracovania dát z kanála, ktorý je pripravený. Okrem toho, pomocou sync.WaitGroup je možné jednoducho počkať, kým sa všetky goroutiny dokončia, čím sa ľahko spravuje pracovný tok. Vďaka týmto nástrojom je možné predchádzať problémom s dátovou konkurenciou medzi vláknami a bezpečnejšie spracovávať súbežnosť.

Okrem toho, pomocou context je možné riadiť životný cyklus, zrušenie, timeout, deadline a rozsah požiadaviek na úrovni používateľa (User-Level), čím sa zabezpečuje určitá úroveň stability.

Paralelné spracovanie Goroutine (GOMAXPROCS)

Hoci som hovoril o výhodách súbežnosti goroutine, možno vás napadne, či nepodporuje aj paralelizmus. V súčasnosti počet jadier CPU presahuje dvojciferné čísla, na rozdiel od minulosti, a aj domáce PC majú značný počet jadier.

Goroutine však vykonáva aj paralelné úlohy, a to prostredníctvom GOMAXPROCS.

Ak nie je GOMAXPROCS nastavený, jeho hodnota sa líši v závislosti od verzie.

  1. Pred verziou 1.5: Predvolená hodnota 1, ak je potrebná hodnota väčšia ako 1, je nevyhnutné ju nastaviť pomocou runtime.GOMAXPOCS(runtime.NumCPU()).

  2. Verzie 1.5 až 1.24: Zmenilo sa na všetky dostupné logické jadrá. Odvtedy už vývojári nemusia nastavovať, pokiaľ to nie je nevyhnutné.

  3. Verzia 1.25: Ako je pre populárny jazyk v kontajnerovom prostredí typické, kontroluje cGroup v systéme Linux a overuje CPU limit nastavený pre kontajner.

    Ak je teda počet logických jadier 10 a limit CPU je 5, GOMAXPROCS sa nastaví na nižšiu hodnotu 5.

Úprava vo verzii 1.25 prináša veľmi významné zmeny. Predovšetkým sa zvýšila využiteľnosť jazyka v kontajnerovom prostredí. To umožnilo predísť zbytočnému vytváraniu vlákien a prepínaniu kontextu, čím sa 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: začiatok\n", name) // Goroutine %d: start
14	time.Sleep(10 * time.Millisecond) // oneskorenie pre simuláciu práce
15	fmt.Printf("Goroutine %d: koniec\n", name) // Goroutine %d: end
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // používať 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 na dokončenie všetkých goroutinov...") // Waiting for all goroutines to finish...
29	wg.Wait()
30	fmt.Println("Všetky úlohy boli dokončené.") // All tasks have been completed.
31
32}
33

Plánovač Goroutine (model M:N)

V predchádzajúcej časti, ktorá sa týka prideľovania a správy OS vlákien pomocou modelu M:N, sa podrobnejšie zameriame na model goroutine GMP.

  • G (Goroutine): Najmenšia jednotka práce vykonávaná v Go
  • M (Machine): OS vlákno (skutočné miesto vykonávania práce)
  • P (Processor): Logický proces riadený Go runtime

P navyše obsahuje lokálnu frontu úloh (Local Run Queue) a funguje ako plánovač, ktorý prideľuje priradené G k M. Zjednodušene, goroutine je

Postup fungovania GMP je nasledovný:

  1. Po vytvorení G (Goroutine) sa pridelí do lokálnej fronty úloh P (Processor).
  2. P (Processor) pridelí G (Goroutine) z lokálnej fronty úloh k M (Machine).
  3. M (Machine) vráti stav G (Goroutine), ktorý môže byť block, complete, preempted.
  4. Work-Stealing (kradnutie úloh): Ak sa lokálna fronta úloh P vyprázdni, iné P skontrolujú globálnu frontu. Ak ani tam nie je G (Goroutine), ukradnú úlohu z iného lokálneho P (Processor), aby všetky M nepretržite pracovali.
  5. Spracovanie systémového volania (Blocking): Ak počas vykonávania G (Goroutine) dôjde k Blocku, M (Machine) prejde do stavu čakania. V tomto okamihu P (Processor) oddelí zablokované M (Machine) a spojí sa s iným M (Machine), aby spustil ďalšie G (Goroutine). Vďaka tomu nedochádza k plytvaniu CPU ani počas doby čakania pri I/O operáciách.
  6. Ak jedno G (Goroutine) dlho pretrváva v stave preempted, dá príležitosť na vykonanie inému G (Goroutine).

Golang má aj GC (Garbage Collector), ktorý beží na 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 je jednou z najsilnejších výhod jazyka, a okrem toho existuje mnoho ďalších, takže dúfam, že si ho mnohí vývojári užijú.

Ďakujem.