Werkingsprincipe van Go goroutines: variabele stack van 2KB en de M:N GMP-scheduler

Goroutine
Wanneer je Gophers vraagt om de voordelen van Golang te noemen, verschijnt er vaak een artikel over concurrency (Concurrency). De basis daarvan is de goroutine (goroutine), die lichtgewicht en eenvoudig te verwerken is. Hierover heb ik een beknopt overzicht geschreven.
Concurrency (Concurrency) vs Parallelism (Parallelism)
Voordat we goroutines begrijpen, willen we eerst twee concepten bespreken die vaak met elkaar worden verward.
- Concurrency: Concurrency gaat over het tegelijkertijd verwerken van veel taken. Het betekent niet noodzakelijkerwijs dat ze daadwerkelijk gelijktijdig worden uitgevoerd, maar is een structureel, logisch concept waarbij meerdere taken in kleine eenheden worden opgedeeld en om de beurt worden uitgevoerd, zodat het voor de gebruiker lijkt alsof meerdere taken tegelijk worden verwerkt. Concurrency is ook mogelijk op een enkele core.
- Parallelism: Parallelism is "het tegelijkertijd verwerken van meerdere taken op meerdere cores". Letterlijk betekent het dat werk parallel wordt uitgevoerd, waarbij andere taken gelijktijdig worden uitgevoerd.
Goroutines maken concurrency eenvoudig te realiseren via de Go runtime scheduler, en door de GOMAXPROCS-instelling maken ze ook op natuurlijke wijze gebruik van parallelism.
Multithreading in Java (Multi thread), dat veel wordt gebruikt, is een representatief concept van parallelism.
Waarom zijn goroutines goed?
Lichtgewicht (lightweight)
De kosten van het aanmaken zijn aanzienlijk lager dan in andere talen. Waarom gebruikt Golang er zo weinig? Dat is de vraag die opkomt, en het antwoord is dat de aanmaak wordt beheerd binnen de Go runtime. Omdat het een lichtgewicht logische thread is, is het kleiner dan een OS-thread, vereist de initiële stack ongeveer 2KB, en wordt de stack dynamisch vergroot naar behoefte, afhankelijk van de implementatie van de gebruiker.
Doordat het per stack wordt beheerd, kunnen het aanmaken en verwijderen zeer snel en goedkoop gebeuren, waardoor het zelfs met miljoenen goroutines draaien geen belasting vormt. Hierdoor kan de Goroutine dankzij de runtime scheduler de tussenkomst van de OS-kernel tot een minimum beperken.
Prestaties (performance)
Allereerst, zoals hierboven uitgelegd, heeft de Goroutine weinig tussenkomst van de OS-kernel, waardoor context switching op gebruikersniveau (User-Level) goedkoper is dan op OS-threadniveau, en men snel tussen taken kan wisselen.
Daarnaast beheert het de goroutines via het M:N-model door ze aan OS-threads toe te wijzen. Door een OS-threadpool te maken zijn er niet veel threads nodig; met een klein aantal threads kan alles worden verwerkt. Wanneer bijvoorbeeld een wachttoestand optreedt, zoals een systeemaanroep, voert de Go runtime een andere goroutine uit op de OS-thread, zodat de OS-thread niet stilligt en de CPU efficiënt wordt benut, wat zorgt voor snelle verwerking.
Hierdoor kan Golang, vooral bij I/O-bewerkingen, een hogere prestatie leveren dan andere talen.
Beknopt (concise)
Wanneer concurrency nodig is, is het ook een groot voordeel dat een functie eenvoudig kan worden verwerkt met slechts het sleutelwoord go.
Men moet complexe locks gebruiken, zoals Mutex en Semaphore, en bij het gebruik van locks is men onvermijdelijk genoodzaakt rekening te houden met de deadlock (DeadLock)-toestand, waardoor het ontwerp vanaf de ontwerpfase al complex wordt.
Goroutine moedigt, volgens de filosofie "Deel geheugen door te communiceren, in plaats van te communiceren door geheugen te delen", het gegevenstransport via Channel aan, en SELECT ondersteunt zelfs de functie die in combinatie met channels ervoor zorgt dat verwerking kan beginnen bij het kanaal waarvoor de gegevens gereed zijn. Bovendien kan men met sync.WaitGroup eenvoudig wachten tot alle goroutines zijn afgerond, waardoor de werkstroom gemakkelijk kan worden beheerd. Dankzij deze hulpmiddelen kunnen dataraces tussen threads worden voorkomen en kan concurrency veiliger worden verwerkt.
Ook kan men via context de levenscyclus, annulering, time-outs, deadlines en het verzoekbereik op gebruikersniveau (User-Level) beheersen, wat enige mate van stabiliteit garandeert.
Parallel werken van Goroutine (GOMAXPROCS)
Na de voordelen van de concurrency van goroutines te hebben besproken, zal de vraag opkomen of parallelisme niet wordt ondersteund? Tegenwoordig is het aantal CPU-cores tweecijferig geworden, en ook consumenten-pc's hebben een flink aantal cores.
Goroutine voert echter ook parallel werk uit; dat wordt verzorgd door GOMAXPROCS.
Als GOMAXPROCS niet wordt ingesteld, wordt het per versie verschillend ingesteld.
Vóór 1.5: standaardwaarde 1; om meer dan 1 te gebruiken is instelling via zoiets als
runtime.GOMAXPOCS(runtime.NumCPU())verplicht1.5 ~ 1.24: gewijzigd naar het aantal beschikbare logische cores. Vanaf dat moment hoeft de ontwikkelaar het niet in te stellen, tenzij er specifieke beperkingen nodig zijn
1.25: passend bij een taal die populair is in containeromgevingen, controleert het de door cGroup op Linux ingestelde
CPU-limietvan de container.Als het aantal logische cores bijvoorbeeld 10 is en de CPU-limiet 5, dan wordt
GOMAXPROCSingesteld op 5, het lagere getal.
De wijziging in 1.25 is een zeer belangrijke aanpassing, omdat het gebruik van de taal in containeromgevingen is verbeterd. Hierdoor kan het onnodig aanmaken van threads en context switching worden verminderd, waardoor CPU-throttling kan worden voorkomen.
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)
14 time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연
15 fmt.Printf("Goroutine %d: 시작\n", name)
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용
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이 끝날 때까지 대기합니다...")
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.")
31
32}
33
Scheduler van Goroutine (M:N-model)
Het eerdere deel waarin wordt gesteld dat goroutines via het M:N-model aan OS-threads worden toegewezen en beheerd, wordt concreter met het GMP-model van goroutines.
- G (Goroutine): De kleinste werkeenheid die in Go wordt uitgevoerd
- M (Machine): OS-thread (de daadwerkelijke werklocatie)
- P (Processor): Een logisch proces dat door de Go runtime wordt beheerd
Het P heeft bovendien een lokale uitvoeringswachtrij (Local Run Queue) en fungeert als scheduler die de toegewezen G aan een M toewijst. Kortom, een goroutine verloopt als volgt via GMP.
Het verloop van GMP is als volgt:
- Wanneer een G (Goroutine) wordt aangemaakt, wordt deze toegewezen aan de lokale uitvoeringswachtrij van een P (Processor).
- De P (Processor) wijst de G (Goroutine) in de lokale uitvoeringswachtrij toe aan een M (Machine).
- De M (Machine) retourneert de status van de G (Goroutine): block, complete of preempted.
- Work-Stealing (werk stelen): Als de lokale uitvoeringswachtrij van een P leeg raakt, controleert een andere P eerst de globale wachtrij. Als daar ook geen G (Goroutine) staat, steelt het werk van de lokale P (Processor) van een ander, zodat alle M's voortdurend actief blijven.
- Systeemaanroepverwerking (Blocking): Wanneer een G (Goroutine) tijdens de uitvoering blokkeert, komt de M (Machine) in een wachttoestand. Op dat moment wordt de P (Processor) losgekoppeld van de geblokkeerde M (Machine) en gekoppeld aan een andere M (Machine), om de volgende G (Goroutine) uit te voeren. Hierbij is er ook tijdens wachttijden bij I/O-bewerkingen geen verspilling van CPU.
- Wanneer een G (Goroutine) lang wordt voorgetrokken (preempted), krijgt een andere G (Goroutine) de kans om uit te voeren.
Golang voert ook de GC (Garbage Collector) uit op goroutines, waardoor geheugen parallel kan worden opgeruimd terwijl de uitvoering van de applicatie zo min mogelijk wordt onderbroken (STW), wat systeembronnen efficiënt gebruikt.
Tot slot is Golang een van de sterke punten van de taal, en er zijn nog veel meer voordelen; ik hoop dat veel ontwikkelaars van Go plezier zullen beleven.
Dank u wel.