GoSuda

Go rutinok alapjai

By hamori
views ...

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. 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.

  2. 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.

  3. 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ő:

  1. Amikor egy G (Goroutine) létrejön, hozzárendelésre kerül a P (Processor) lokális végrehajtási sorához.
  2. A P (Processor) hozzárendeli a lokális végrehajtási sorban lévő G-t (Goroutine) az M-hez (Machine).
  3. Az M (Machine) visszaadja a G (Goroutine) állapotát: block, complete, preempted.
  4. 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.
  5. 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.
  6. 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.