Go rutinok alapjai
Goroutine
Gopher들에게 golang의 장점을 이야기 해달라하면 자주 등장하는 동시성(Concurrency) 관련 글이 있습니다. 그 내용의 기반은 가볍고 간단하게 처리할 수 있는 **고루틴(goroutine)**입니다. 이에 대하여 간략하게 작성해보았습니다.
동시성(Concurrency) vs 병렬성(Parallelism)
고루틴을 이해하기 전에, 자주 혼동되는 두 가지 개념et először tisztázni szeretném.
- 동시성: 동시esség azt jelenti, hogy sok feladatot egyszerre kezelünk. Ez nem feltétlenül jelenti azt, hogy a feladatok ténylegesen egyidejűleg futnak, hanem egy strukturális és logikai koncepció, amelyben 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 feladat egyidejűleg futna. A konkurens végrehajtás egyetlen magon is lehetséges.
- 병렬성: A párhuzamosság azt jelenti, hogy „több feladatot egyszerre hajtunk végre több magon”. Szó szerint párhuzamosan végezzük a feladatokat, azaz más feladatokat egyidejűleg futtatunk.
A goroutine-ok lehetővé teszik a konkurens végrehajtás egyszerű implementálását a Go futásidejű ütemezőn keresztül, és a GOMAXPROCS
beállítás használatával természetesen kihasználják a párhuzamosságot is.
A Java Multi thread, amely gyakran magas kihasználtsággal rendelkezik, a párhuzamosság tipikus koncepciója.
고루틴은 왜 좋을까?
가볍다(lightweight)
A létrehozási költsége nagyon alacsony más nyelvekhez képest. Felmerülhet a kérdés, hogy miért használja a golang ilyen kevéssé? Ennek az az oka, hogy a létrehozás helyét a Go futásidejű környezet kezeli. Mivel ez egy könnyű logikai szál, kisebb, mint az OS szál egység, és a kezdeti stack 2KB méretű, dinamikusan változhat a felhasználó implementációjának megfelelően.
A stack-alapú kezelésnek köszönhetően a létrehozás és törlés nagyon gyors és olcsó, így több millió goroutine futtatása sem jelent terhet. Ennek eredményeként a Goroutine minimálisra csökkentheti az OS kernel beavatkozását a futásidejű ütemezőnek köszönhetően.
성능이 좋다(performance)
Először is, amint fentebb említettük, a Goroutine minimális OS kernel beavatkozással jár, így a felhasználói szintű (User-Level) kontextusváltás olcsóbb, mint az OS szál egységek esetében, ami gyors feladatváltást tesz lehetővé.
Ezenkívül az M:N modellt használja az OS szálakhoz való hozzárendeléshez és kezeléshez. Az OS szálkészlet létrehozásával kevés szál is elegendő a feldolgozáshoz, anélkül, hogy sok szálra lenne szükség. Például, ha egy rendszerhívás, mint például egy I/O művelet blokkoló állapotba kerül, a Go futásidejű környezet egy másik goroutine-t futtat az OS szálon, így az OS szál folyamatosan, hatékonyan használja a CPU-t, ami gyors feldolgozást eredményez.
Ezért a Golang különösen magas teljesítményt nyújthat I/O műveletekben más nyelvekhez képest.
간결하다(concise)
Nagy előny az is, hogy a go
kulcsszóval könnyedén kezelhetők a funkciók, ha konkurens végrehajtásra van szükség.
Olyan komplex zárakat kell használni, mint a Mutex
és a Semaphore
, és a zárak használatakor elkerülhetetlenül figyelembe kell venni a DeadLock állapotot, ami már a fejlesztési tervezési szakaszában is komplex lépéseket igényel.
A Goroutine a "Ne a memória megosztásával kommunikálj, hanem a kommunikációval oszd meg a memóriát" filozófiát követve a Channel
-en keresztüli adatátvitelt javasolja, és a SELECT
a Channel-lel kombinálva támogatja azt a funkciót, hogy az adatokat a már elkészült Channel-ből dolgozza fel. Ezenkívül a sync.WaitGroup
használatával egyszerűen meg lehet várni, amíg az összes goroutine befejeződik, ami megkönnyíti a feladatfolyamatok kezelését. Ezeknek az eszközöknek köszönhetően elkerülhetők a szálak közötti adatverseny-problémák, és biztonságosabbá válik a konkurens feldolgozás.
Ezenkívül a kontextus (context) használatával felhasználói szinten (User-Level) szabályozható az életciklus, a megszakítás, a timeout, a határidő és a kérés hatóköre, ami bizonyos fokú stabilitást biztosít.
Goroutine의 병렬 작업(GOMAXPROCS)
Bár a goroutine-ok konkurens végrehajtásának előnyeiről beszéltünk, felmerülhet a kérdés, hogy vajon nem támogatják-e a párhuzamos végrehajtást? Ennek oka, hogy a modern CPU-k magjai már nem egyszámjegyűek, és az otthoni számítógépek sem tartalmaznak kevés magot.
A Goroutine azonban párhuzamos végrehajtást is végez, ez a GOMAXPROCS
.
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ék 1, ha 1-nél többre van szükség, akkor kötelező beállítani, például
runtime.GOMAXPOCS(runtime.NumCPU())
módon.1.5 ~ 1.24: Az összes elérhető logikai magra változott. Ettől kezdve a fejlesztőknek nem kell beállítaniuk, hacsak nem szükséges.
1.25: A konténeres környezetben népszerű nyelvként ellenőrzi a cGroup-ot Linuxon, és megnézi a konténerre beállított
CPU korlátozást
.Ekkor, ha a logikai magok száma 10, és a CPU korlátozás értéke 5, akkor a
GOMAXPROCS
az alacsonyabb számra, azaz 5-re lesz beállítva.
Az 1.25-ös módosítás rendkívül jelentős változásokat hoz. Ez növelte a nyelv használhatóságát konténeres környezetben. Ezáltal csökkenthető a szükségtelen szálak létrehozása és a kontextusváltás, megelőzve a CPU throttlingot.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "sync" // sync csomag importálása
8 "time"
9)
10
11func exe(name int, wg *sync.WaitGroup) {
12 defer wg.Done() // Jelzi a WaitGroup-nak, hogy ez a goroutine befejeződött
13
14 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine kezdete
15 time.Sleep(10 * time.Millisecond) // Késleltetés a feladat szimulációjához
16 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine kezdete
17}
18
19func main() {
20 runtime.GOMAXPROCS(2) // Csak 2 CPU mag használata
21 wg := sync.WaitGroup(); // WaitGroup inicializálása
22 goroutineCount := 10 // Goroutine-ok száma
23 wg.Add(goroutineCount) // Hozzáadja a goroutine-ok számát a WaitGroup-hoz
24
25 for i := 0; i < goroutineCount; i++ {
26 go exe(i, &wg) // Goroutine indítása
27 }
28
29 fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Vár minden goroutine befejezésére
30 wg.Wait() // Vár, amíg az összes goroutine befejeződik
31 fmt.Println("모든 작업이 완료되었습니다.") // Minden feladat befejeződött
32
33}
34
Goroutine의 스케쥴러 (M:N모델)
Az előző rész, amely az M:N modell OS szálakra való leképezésével és kezelésével foglalkozik, részletesebben a goroutine GMP modelljére tér ki.
- G (Goroutine): A Go-ban futó legkisebb végrehajtási egység.
- M (Machine): OS szál (a tényleges végrehajtás helye).
- P (Processor): A Go futásidejű környezet által kezelt logikai processzor.
A P ezenfelül egy lokális végrehajtási sorral (Local Run Queue) is rendelkezik, és ütemezőként működik, amely a hozzárendelt G-ket az M-ekhez rendeli. Egyszerűen, a goroutine:
A GMP működési folyamata a következő:
- Amikor egy G (Goroutine) létrejön, hozzárendelésre kerül a P (Processor) lokális végrehajtási sorához.
- A P (Processor) hozzárendeli a lokális végrehajtási sorban lévő G-t (Goroutine) az M-hez (Machine).
- Az M (Machine) visszaadja a G (Goroutine) állapotát: block, complete, preempted.
- Work-Stealing (feladatlopás): Ha a P lokális végrehajtási sora kiürül, egy másik P ellenőrzi a globális sort. Ha ott sincs G (Goroutine), akkor ellopja egy másik lokális P (Processor) feladatát, így minden M folyamatosan működik.
- Rendszerhívás-kezelés (Blocking): Ha egy G (Goroutine) végrehajtása során Block történik, az M (Machine) várakozó állapotba kerül, ekkor a P (Processor) leválasztja a Blockolt M-et (Machine) és egy másik M-mel (Machine) párosulva végrehajtja a következő G-t (Goroutine). Ekkor az I/O műveletek várakozási ideje alatt sincs CPU-pazarlás.
- Ha egy G (Goroutine) hosszú ideig előre meghatározott (preempted), akkor más G-knek (Goroutine) is lehetőséget ad a végrehajtásra.
A Golang GC (Garbage Collector) is Goroutine-on fut, minimálisra csökkentve az alkalmazás megszakítását (STW), és párhuzamosan tudja tisztítani a memóriát, ami hatékony rendszererőforrás-felhasználást eredményez.
Végezetül, a Golang az egyik nagy előnye a nyelvek között, és sok más előnye is van, remélem, sok fejlesztő fogja élvezni a Go-t.
Köszönöm.