GoSuda

Grunderna i Goroutines

By hamori
views ...

Goroutine

När man ber Gophers att berätta om fördelarna med Go dyker ofta artiklar om samtidighet (Concurrency) upp. Grunden för detta är den lätta och enkla hanteringen av goroutine. Jag har kortfattat skrivit om detta.

Samtidighet (Concurrency) vs. Parallellism (Parallelism)

Innan vi förstår goroutines vill jag först förklara två ofta förvirrade begrepp.

  • Samtidighet: Samtidighet handlar om att hantera många uppgifter samtidigt. Det betyder inte nödvändigtvis att de faktiskt körs samtidigt, utan det är ett strukturellt och logiskt koncept där flera uppgifter delas upp i mindre enheter och körs omväxlande, så att det för användaren ser ut som om flera uppgifter behandlas samtidigt. Samtidighet är möjlig även på en enskild kärna.
  • Parallellism: Parallellism innebär "att behandla flera uppgifter samtidigt på flera kärnor". Det är bokstavligen att utföra arbete parallellt och köra olika uppgifter samtidigt.

Goroutines gör det enkelt att implementera samtidighet via Go runtime-schemaläggaren och utnyttjar parallellism naturligt genom GOMAXPROCS-inställningen.

Javas multi-threading, som ofta används mycket, är ett typiskt exempel på parallellism.

Varför är goroutines bra?

Lätta (lightweight)

Kostnaden för att skapa dem är mycket låg jämfört med andra språk. Frågan uppstår varför Go använder så lite; det beror på att skapandet hanteras internt av Go runtime. Detta beror på att det är en lättviktig logisk tråd, mindre än en OS-tråd, och den initiala stacken kräver cirka 2KB utrymme och kan dynamiskt variera genom att lägga till stackar beroende på användarens implementering.

Genom att hantera dem i stackenheter kan skapande och borttagning ske mycket snabbt och billigt, vilket gör det möjligt att hantera miljontals goroutines utan att det blir betungande. Detta gör att Goroutine kan minimera OS-kärnans inblandning tack vare runtime-schemaläggaren.

Bra prestanda (performance)

Först och främst, som beskrivits ovan, involverar Goroutine mindre inblandning från OS-kärnan, vilket gör kontextväxling på användarnivå billigare än på OS-trådnivå, vilket möjliggör snabba uppgiftsväxlingar.

Dessutom hanteras de genom att tilldelas OS-trådar med hjälp av M:N-modellen. Genom att skapa en OS-trådpool kan bearbetning utföras med färre trådar utan behov av många trådar. Om en goroutine hamnar i ett väntetillstånd, som till exempel ett systemanrop, kör Go runtime en annan goroutine på OS-tråden, vilket gör att OS-tråden inte vilar och effektivt utnyttjar CPU:n för snabb bearbetning.

Detta gör att Golang kan uppnå högre prestanda än andra språk, särskilt vid I/O-operationer.

Kortfattade (concise)

En stor fördel är också att en funktion enkelt kan hanteras med ett enda go-nyckelord när samtidighet behövs.

Komplexa lås som Mutex och Semaphore måste användas, och när lås används måste man nödvändigtvis ta hänsyn till dödlägen (DeadLock), vilket kräver komplexa steg redan i designfasen före utvecklingen.

Goroutine rekommenderar dataöverföring via Channel enligt filosofin "Kommunicera genom att inte dela minne, dela minne genom att kommunicera". SELECT stöder även funktionalitet som i kombination med Channel möjliggör bearbetning från den kanal där data är redo. Dessutom, med sync.WaitGroup, kan man enkelt vänta tills alla goroutines är klara, vilket underlättar hanteringen av arbetsflödet. Tack vare dessa verktyg kan datakapplöpningsproblem mellan trådar förhindras och en säkrare samtidig bearbetning uppnås.

Dessutom kan man med hjälp av context styra livscykeln, avbrytning, timeout, deadlines och begärandeomfång på användarnivå, vilket garanterar en viss grad av stabilitet.

Goroutines parallella arbete (GOMAXPROCS)

Även om vi har talat om fördelarna med goroutines samtidighet, kanske du undrar om de inte stöder parallellitet. Antalet kärnor i moderna CPU:er är nu tvåsiffrigt, och även hemdatorer har ett betydande antal kärnor.

Men Goroutine utför även parallella operationer, och det är GOMAXPROCS.

Om GOMAXPROCS inte är inställt, konfigureras det annorlunda beroende på versionen.

  1. Före 1.5: Standardvärde 1. Om mer än 1 behövs är det obligatoriskt att ställa in det med metoder som runtime.GOMAXPOCS(runtime.NumCPU()).

  2. 1.5 till 1.24: Ändrades till alla tillgängliga logiska kärnor. Från och med nu behöver utvecklare inte ställa in det om det inte finns ett mycket specifikt behov.

  3. 1.25: Som ett välkänt språk i container-miljöer, kontrollerar det cGroup i Linux för att bekräfta CPU-begränsningen som är inställd i containern.

    Om antalet logiska kärnor är 10 och CPU-begränsningen är 5, ställer GOMAXPROCS in sig på det lägre värdet 5.

Ändringen i 1.25 är en mycket stor ändring. Det beror på att språkets användbarhet i container-miljöer har ökat. Detta har gjort det möjligt att förhindra onödig trådskapande och kontextväxling, vilket minskar CPU-strypning (throttling).

 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: Startar
14	time.Sleep(10 * time.Millisecond) // Fördröjning för att simulera arbete
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Startar
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // Använder endast 2 CPU-kärnor
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이 끝날 때까지 대기합니다...") // Väntar tills alla goroutines är klara...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // Alla uppgifter är klara.
31
32}
33

Goroutines schemaläggare (M:N-modell)

För att fördjupa oss i den tidigare nämnda M:N-modellen för tilldelning och hantering av OS-trådar finns det en goroutine GMP-modell.

  • G (Goroutine): Den minsta arbetsenheten som körs i Go
  • M (Machine): OS-tråd (den faktiska arbetsplatsen)
  • P (Processor): En logisk process som hanteras av Go runtime

P har dessutom en lokal körkö (Local Run Queue) och fungerar som en schemaläggare som tilldelar de allokerade G till M. Enkelt uttryckt är goroutine:

GMP:s funktionssätt är följande:

  1. När en G (Goroutine) skapas, tilldelas den till P:s (Processor) lokala körkö.
  2. P (Processor) tilldelar G (Goroutine) från sin lokala körkö till M (Machine).
  3. M (Machine) returnerar G:s (Goroutine) status: block, complete, preempted.
  4. Work-Stealing (Arbetsstöld): Om P:s lokala körkö blir tom, kontrollerar en annan P den globala kön. Om det inte finns någon G (Goroutine) där heller, stjäl den arbete från en annan lokal P (Processor) för att se till att alla M arbetar utan uppehåll.
  5. Systemanrop (Blocking): Om en G (Goroutine) blockeras under exekvering, hamnar M (Machine) i ett väntetillstånd. Då separeras P (Processor) från den blockerade M (Machine) och kombineras med en annan M (Machine) för att köra nästa G (Goroutine). Vid denna tidpunkt slösas ingen CPU-tid under väntetiden för I/O-operationer.
  6. Om en G (Goroutine) upptar resurser (preempted) under lång tid, ges exekveringstillfället till en annan G (Goroutine).

Golang:s GC (Garbage Collector) körs också på Goroutine, vilket gör att minnet kan rensas parallellt med minimala avbrott i applikationens exekvering (STW), vilket effektivt utnyttjar systemresurser.

Slutligen är Golang en av språkets starka fördelar, och det finns många fler. Jag hoppas att många utvecklare kommer att njuta av Go.

Tack.