A GoRUTIN alapjai
Goroutine
Ha megkérjük a Gophereket, hogy beszéljenek a golang előnyeiről, gyakran felmerül a konkurencia (Concurrency) témája. Ennek alapja a könnyű és egyszerűen kezelhető goroutine. Erről írtam egy rövid összefoglalót.
Konkurencia (Concurrency) vs. Párhuzamosság (Parallelism)
Mielőtt megértenénk a goroutine-t, szeretném tisztázni azt a két fogalmat, amelyet gyakran összekevernek.
- Konkurencia: A konkurencia arról szól, hogy sok feladatot kezelünk egyszerre. Ez nem feltétlenül jelenti azt, hogy ténylegesen egyszerre futnak, hanem egy strukturális, logikai koncepció, amely szerint több feladatot kisebb egységekre osztunk, és felváltva futtatjuk őket, így a felhasználó számára úgy tűnik, mintha több feladatot dolgoznánk fel egyszerre. A konkurencia egy magos (single core) rendszeren is lehetséges.
- Párhuzamosság: A párhuzamosság azt jelenti, hogy „több magon egyszerre több feladatot dolgozunk fel”. Szó szerint párhuzamosan történik a feladatok végrehajtása, és a különböző feladatok egyidejűleg futnak.
A goroutine lehetővé teszi a konkurencia egyszerű megvalósítását a Go runtime ütemezőjén keresztül, és a GOMAXPROCS
beállítás révén természetesen kihasználja a párhuzamosságot is.
A nagy kihasználtságú Java Multi thread (többszálú) megoldása a párhuzamosság tipikus koncepciója.
Miért jó a goroutine?
Könnyű (lightweight)
A létrehozási költsége rendkívül alacsony más nyelvekhez képest. Felmerül a kérdés, hogy miért használ kevesebbet a golang? Ennek oka, hogy a létrehozás a Go runtime belsejében történik, és ott is kezelik. Mivel az előbb említett könnyű logikai szálról van szó, kisebb, mint az OS thread egység, kezdeti stack mérete körülbelül 2 KB, és a felhasználói implementációtól függően dinamikusan növekedhet, ha stack-et adunk hozzá.
A stack egységekben történő kezelésnek köszönhetően a létrehozás és eltávolítás nagyon gyors és olcsó, így akár több millió goroutine futtatása sem jelent terhet. Ennek köszönhetően a Goroutine a runtime ütemezőnek köszönhetően minimálisra csökkentheti az OS kernel beavatkozását.
Jó a teljesítménye (performance)
Először is, ahogy fentebb említettük, a Goroutine kevesebb OS kernel beavatkozást igényel, így a User-Level (felhasználói szintű) kontextusváltáskor olcsóbb, mint az OS thread egység, ami gyorsabb feladatváltást tesz lehetővé.
Ezen kívül M:N modellt használ, és az OS thread-ekhez rendelve kezeli azokat. Létrehoz egy OS thread poolt, ami lehetővé teszi a feldolgozást kevés szál (thread) segítségével is, anélkül, hogy sok szálra lenne szükség. Például, ha egy goroutine várakozó állapotba kerül, mint például egy rendszerhívás (system call) során, a Go runtime egy másik goroutine-t futtat az OS thread-en, így az OS thread nem tétlenkedik, hatékonyan kihasználja a CPU-t, és gyors feldolgozást tesz lehetővé.
Ennek eredményeként a Golang különösen az I/O műveletek terén képes magasabb teljesítményt nyújtani más nyelvekhez képest.
Tömör (concise)
Nagy előny, hogy ha konkurenciára van szükség, a go
kulcsszóval könnyedén kezelhető egy függvény.
Komplex Lock-ok, mint például Mutex
, Semaphore
használata szükséges, és ha Lock-ot használunk, elkerülhetetlenül figyelembe kell venni a DeadLock (holtpont) állapotát, ami már a tervezési szakaszban bonyolult lépéseket igényel.
A Goroutine a "Ne memóriát ossz meg a kommunikációhoz, hanem kommunikálj a memória megosztásához" filozófia szerint a Channel
(csatorna) útján történő adatátvitelt javasolja, és a SELECT
a Channel-lel kombinálva támogatja azt a funkciót, hogy először a már elkészült Channel-ből érkező adatokat dolgozza fel. Ezenkívül a sync.WaitGroup
használatával egyszerűen megvárható, amíg több goroutine is befejezi a munkát, így a munkafolyamat könnyen kezelhető. Ezeknek az eszközöknek köszönhetően megelőzhető az adatok közötti versenyhelyzet a szálak között, és biztonságosabb a konkurencia kezelése.
Továbbá, a context
(kontextus) használatával felhasználói szinten (User-Level) vezérelhető az életciklus, a megszakítás, a időtúllépés (timeout), a határidő (deadline) és a kérelem hatóköre, ami bizonyos fokú stabilitást garantál.
A Goroutine párhuzamos munkája (GOMAXPROCS
)
Bár a goroutine konkurenciájának előnyeiről beszéltünk, felmerülhet a kérdés, hogy nem támogatja-e a párhuzamosságot. A modern CPU-k magjainak száma eltér a régitől, és már két számjegyű, sőt, az otthoni PC-kben is jelentős számú mag található.
A Goroutine azonban párhuzamos munkát is végez, ez a GOMAXPROCS
.
Ha a GOMAXPROCS
nincs beállítva, a verziótól függően eltérő beállítás érvényesül:
1.5 előtt: Alapértelmezett érték 1, és 1-nél nagyobb érték esetén a beállítás kötelező, például
runtime.GOMAXPOCS(runtime.NumCPU())
módon.1.5 – 1.24: Az összes rendelkezésre álló logikai mag számára módosult. Ettől az időszaktól kezdve a fejlesztőnek nem kell beállítania, hacsak nem szükséges valamilyen komoly korlátozás.
1.25: A konténer környezetben híres nyelvhez méltóan ellenőrzi a linux-os cGroup-ot, és megnézi a konténerre beállított
CPU limitet
.Ha a logikai magok száma 10, és a CPU limit 5, akkor a
GOMAXPROCS
a kisebb értékre, azaz 5-re állítódik be.
Az 1.25-ös módosítás rendkívül jelentős változást hozott. Ezáltal megnőtt a nyelv kihasználtsága a konténer környezetekben. Ez csökkenti a szükségtelen szál (thread) létrehozását és a kontextusváltást, megelőzve a CPU throttling-ot.
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: Indul
14 time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // Késleltetés a munka szimulálására
15 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Indul
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Csak 2 CPU mag használata
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이 끝날 때까지 대기합니다...") // Megvárjuk, amíg az összes goroutine befejeződik...
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.") // Minden munka befejeződött.
31
32}
33
A Goroutine ütemezője (M:N modell)
A korábbi, M:N modellt használó, OS thread-ekhez rendelt kezelésről szóló részben egy kicsit részletesebben belemegyünk a goroutine GMP modelljébe.
- G (Goroutine): A Go-ban futó legkisebb munkaegység
- M (Machine): OS thread (a tényleges munka helye)
- P (Processor): A Go runtime által kezelt logikai processzor
A P ezen felül rendelkezik egy lokális végrehajtási sorral (Local Run Queue), és ütemezőként szolgál, amely a hozzárendelt G-t hozzárendeli az M-hez. Egyszerűen fogalmazva, a goroutine a következőképpen működik:
A GMP működése a következő:
- Amikor G (Goroutine) jön létre, hozzárendelésre kerül a P (Processor) lokális végrehajtási sorához.
- A P (Processor) a lokális végrehajtási sorban lévő G (Goroutine) -t hozzárendeli az M (Machine) -hez.
- Az M (Machine) visszatér a G (Goroutine) állapotával:
block
,complete
,preempted
. - Work-Stealing (Munka lopás): Ha a P lokális végrehajtási sora kiürül, a másik P ellenőrzi a globális sort. Ha ott sincs G (Goroutine), akkor a másik lokális P (Processor) munkáját ellopja, biztosítva, hogy minden M folyamatosan dolgozzon.
- Rendszerhívás kezelése (Blocking): Ha a G (Goroutine) futása közben Block következik be, az M (Machine) várakozó állapotba kerül, ekkor a P (Processor) leválasztja a Block állapotba került M (Machine) -t, és egy másik M (Machine) -hez kapcsolódva elindítja a következő G (Goroutine) -t. Ilyenkor az I/O műveletek során fellépő várakozási idő alatt sincs CPU pazarlás.
- Ha egy G (Goroutine) túl sokáig foglalja le (preempted) a végrehajtást, átadja a futtatási lehetőséget egy másik G (Goroutine) -nak.
A Golang GC-je (Garbage Collector) szintén a Goroutine felett fut, így párhuzamosan tudja a memóriát takarítani, minimalizálva az alkalmazás futásának megszakítását (STW), és hatékonyan használja fel a rendszer erőforrásait.
Végezetül, a Golang az egyik nagy erőssége a nyelvnek, és sok más is van még, remélem, sok fejlesztő élvezni fogja a Go-t.
Köszönöm.