A Goroutine-ok alapjai
Goroutine
Amennyiben a Gopher-ektől azt kérdezzük, mi a golang előnye, gyakran emlegetik a konkurenciát (Concurrency). 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)
A goroutine megértése előtt két, gyakran összetévesztett fogalmat szeretnék tisztázni.
- Konkurencia: A konkurencia arról szól, hogy sok feladatot egyszerre kezelünk. Nem feltétlenül jelenti azt, hogy ténylegesen egyidejűleg futnak, hanem egy strukturális és logikai koncepció, amely során több feladatot kis egységekre osztunk, és felváltva futtatjuk őket, így a felhasználó számára úgy tűnik, mintha egyszerre több feladat is feldolgozásra kerülne. Egyetlen magon is megvalósítható a konkurencia.
- Párhuzamosság: A párhuzamosság „több feladat egyidejű végrehajtását jelenti több magon”. Szó szerint párhuzamosan történik a munka, és más feladatok is egyidejűleg futnak.
A goroutine a Go futásidejű ütemezőjének köszönhetően lehetővé teszi a konkurencia egyszerű megvalósítását, é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álas) megoldása a párhuzamosság tipikus példája.
Miért jó a goroutine?
Könnyű (lightweight)
A létrehozási költsége más nyelvekhez képest rendkívül alacsony. Felmerülhet a kérdés, hogy a golang miért használ kevesebbet? Azért, mert a létrehozás a Go futásidejű környezetén belül történik. Ennek oka, hogy a fent említett könnyű logikai szálakról van szó, amelyek kisebbek az OS szálaknál, kezdeti stackjük körülbelül 2KB méretű, és a felhasználó implementációjától függően dinamikusan bővíthető a stack.
A stack-alapú kezelésnek köszönhetően a létrehozás és eltávolítás rendkívül gyors és olcsó, így akár több millió goroutine futtatása sem jelent terhelést. Ezáltal a Goroutine a futásidejű ütemezőnek köszönhetően minimálisra csökkentheti az OS kernel beavatkozását.
Jó teljesítményű (performance)
Először is, amint a fentiekben említettük, a Goroutine kevesebb OS kernel beavatkozással jár, így a felhasználói szintű (User-Level) kontextusváltás költsége alacsonyabb, mint az OS szálaké, ami gyorsabb feladatváltást tesz lehetővé.
Emellett az M:N modell segítségével az OS szálakhoz rendelve kezeli a feladatokat. Az OS szálkészlet létrehozásával sok szál nélkül, kevés szál segítségével is elvégezhető a feldolgozás. Például, ha egy rendszerhívás, mint például a várakozási állapot, blokkolja a végrehajtást, a Go futásidejű környezet egy másik goroutine-t indít el az OS szálon, így az OS szál nem pihen, hanem hatékonyan használja ki a CPU-t a gyorsabb feldolgozás érdekében.
Ezért a Golang különösen az I/O feladatok során képes magasabb teljesítményt nyújtani más nyelvekhez képest.
Tömör (concise)
Nagy előnye, hogy ha konkurenciára van szükség, a go kulcsszóval könnyedén kezelhetők a funkciók.
Komplex zárakra, mint például Mutex és Semaphore van szükség, és a zárak használatakor elkerülhetetlenül figyelembe kell venni a DeadLock állapotot, ami már a fejlesztést megelőző tervezési fázisban komplex lépéseket igényel.
A Goroutine a "ne oszd meg a memóriát a kommunikációhoz, hanem kommunikálj a memória megosztásához" filozófia szerint a Channel (csatorna) alapú adatátvitelt javasolja, és a SELECT a Channel-lel kombinálva lehetővé teszi, hogy az adatok készen álló csatornáktól kezdve dolgozzák fel a feladatokat. Emellett a sync.WaitGroup segítségével egyszerűen megvárható, amíg az összes goroutine befejeződik, így könnyen kezelhető a munkafolyamat. Ezeknek az eszközöknek köszönhetően elkerülhetők a szálak közötti adatversenyek, és biztonságosabban valósítható meg a konkurencia.
Továbbá, a kontextus (context) segítségével a felhasználói szinten (User-Level) szabályozható az életciklus, a megszakítás, az időtúllépés, a határidő és a kérések hatóköre, ami bizonyos fokú stabilitást biztosít.
A Goroutine párhuzamos feladatai (GOMAXPROCS)
Beszéltünk a goroutine konkurencia előnyeiről, de felmerülhet a kérdés, hogy nem támogatja-e a párhuzamosságot. A modern CPU-k magjainak száma, a múlttal ellentétben, meghaladja a tíz-et, és még az otthoni PC-kben is jelentős számú mag található.
Azonban a Goroutine párhuzamos feladatokat is végez, és ezt a GOMAXPROCS teszi lehetővé.
Ha a GOMAXPROCS nincs beállítva, akkor a verziótól függően eltérően kerül beállításra.
1.5 előtt: Alapértelmezett értéke 1, és ha 1-nél többre volt szükség, akkor kötelező volt beállítani, például
runtime.GOMAXPOCS(runtime.NumCPU())módszerrel.1.5 ~ 1.24: Az összes rendelkezésre álló logikai mag számra változott. Ettől kezdve a fejlesztőknek általában nem kellett beállítani, hacsak nem volt különleges korlátozásra szükségük.
1.25: A konténeres környezetekben népszerű nyelvhez méltóan ellenőrzi a Linux cGroup beállításait, és megvizsgálja a konténerre vonatkozó
CPU korlátozást.Ha például a logikai magok száma 10, és a CPU korlátozás 5, akkor a
GOMAXPROCSa kisebb, 5-ös értékre állítódik be.
Az 1.25-ös módosítás rendkívül jelentős változásokat hozott. Nőtt a nyelv hasznossága a konténeres környezetekben. Ennek eredményeként elkerülhetővé vált a felesleges szálak létrehozása és a kontextusváltás, ezáltal megelőzhető a CPU throttling.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8 "sync" // A sync csomag importálása
9)
10
11func exe(name int, wg *sync.WaitGroup) {
12 defer wg.Done()
13
14 fmt.Printf("Goroutine %d: 시작\n", name)
15 time.Sleep(10 * time.Millisecond) // Késleltetés a munka szimulálásához
16 fmt.Printf("Goroutine %d: 시작\n", name)
17}
18
19func main() {
20 runtime.GOMAXPROCS(2) // Csak 2 CPU mag használata
21 wg := sync.WaitGroup()
22 goroutineCount := 10
23 wg.Add(goroutineCount)
24
25 for i := 0; i < goroutineCount; i++ {
26 go exe(i, &wg)
27 }
28
29 fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Várakozás az összes goroutine befejezésére
30 wg.Wait()
31 fmt.Println("모든 작업이 완료되었습니다.") // Minden feladat befejeződött.
32
33}
34
A Goroutine ütemezője (M:N modell)
Az M:N modell használatáról, amely az OS szálakhoz rendeli és kezeli a goroutine-okat, a korábbiakban már esett szó. Most egy kicsit részletesebben nézzük meg a goroutine GMP modelljét.
- G (Goroutine): A Go-ban futó legkisebb munkaegység
- M (Machine): OS szál (a tényleges munka helye)
- P (Processor): A Go futásidejű környezete által kezelt logikai processzor
A P ezenkívül rendelkezik egy helyi futási sorral (Local Run Queue), és ütemezőként funkcionál, amely a hozzárendelt G-t hozzárendeli az M-hez. Egyszerűen fogalmazva, a goroutine:
A GMP működési folyamata a következő:
- Amikor egy G (Goroutine) létrejön, hozzárendelődik egy P (Processor) helyi futási sorához.
- A P (Processor) a helyi futási sorban lévő G-t (Goroutine) hozzárendeli egy M-hez (Machine).
- Az M (Machine) visszaadja a G (Goroutine) állapotát: block, complete, preempted.
- Work-Stealing (feladatlopás): Ha a P helyi futási sora üres lesz, egy másik P ellenőrzi a globális sort. Ha ott sincs G (Goroutine), akkor ellopja egy másik helyi P (Processor) feladatát, hogy minden M szünet nélkül dolgozzon.
- Rendszerhívás kezelése (Blocking): Ha egy G (Goroutine) futása során blokk következik be, az M (Machine) várakozó állapotba kerül. Ekkor a P (Processor) leválasztja a blokkolt M-et (Machine) és egy másik M-hez (Machine) kapcsolódva futtatja a következő G-t (Goroutine). Ekkor az I/O műveletek várakozási idejében sem pazarolódik a CPU.
- Ha egy G (Goroutine) túl sokáig foglalja el a processzort (preempted), akkor lehetőséget ad más G-knek (Goroutine) a futásra.
A Golang GC (Garbage Collector) is Goroutine-on fut, minimálisra csökkentve az alkalmazás futásának megszakítását (STW), miközben párhuzamosan tisztítja a memóriát, hatékonyan kihasználva a rendszer erőforrásait.
Végül, a Golang az egyik legerősebb nyelvi előny, és sok más is van, ezért remélem, sok fejlesztő fogja élvezni a Go-t.
Köszönöm.