GoSuda

Grundlæggende om Goroutines

By hamori
views ...

Goroutine

Hvis man beder gophere om at fortælle om fordelene ved golang, vil der ofte være en artikel om samtidighed (Concurrency). Grundlaget for dette indhold er den lette og enkle goroutine, som kan håndteres nemt. Jeg har kortfattet beskrevet dette.

Samtidighed (Concurrency) vs. Parallelisme (Parallelism)

Før vi forstår goroutiner, vil jeg først præcisere to begreber, der ofte forveksles.

  • Samtidighed: Samtidighed handler om at håndtere mange opgaver på én gang. Det betyder ikke nødvendigvis, at de udføres samtidigt i bogstavelig forstand, men er snarere et strukturelt og logisk koncept, hvor flere opgaver opdeles i små enheder og udføres skiftevis, så det for brugeren ser ud som om, flere opgaver behandles samtidigt. Samtidighed er mulig selv på en single-core.
  • Parallelisme: Parallelisme er "behandlingen af flere opgaver samtidigt på flere Cores". Det er bogstaveligt talt at udføre arbejde parallelt og eksekvere forskellige opgaver på samme tid.

Goroutiner gør det let at implementere samtidighed gennem Go runtime-scheduleren og udnytter naturligt parallelisme via GOMAXPROCS-indstillingen.

Javas Multi-thread, som ofte har en høj udnyttelsesgrad, er et typisk eksempel på parallelisme.

Hvorfor er goroutiner gode?

De er lette (lightweight)

Oprettelsesomkostningerne er meget lave sammenlignet med andre sprog. Her opstår spørgsmålet: Hvorfor bruger golang færre ressourcer? Det skyldes, at oprettelsesstedet administreres internt i Go runtime. Årsagen er, at det er en letvægts logisk tråd; den er mindre end en OS-trådsenhed, kræver en indledende stakstørrelse på omkring 2KB og er dynamisk variabel ved at tilføje stak i henhold til brugerens implementering.

Ved at blive administreret i stak-enheder er oprettelse og fjernelse meget hurtig og billig, hvilket muliggør håndtering af millioner af goroutiner uden at det er en byrde. Som et resultat minimerer Goroutine indgriben fra OS-kernen takket være runtime-scheduleren.

De har god ydeevne (performance)

For det første, som forklaret ovenfor, har Goroutine mindre indgriben fra OS-kernen, og når kontekstskift udføres på brugerniveau (User-Level), er omkostningerne lavere end for OS-trådsenheder, hvilket muliggør hurtig opgaveskift.

Desuden administreres de ved at blive tildelt OS-tråde ved hjælp af M:N-modellen. Ved at oprette en OS-trådpool er det muligt at håndtere opgaver med færre tråde, uden at der er behov for mange tråde. Hvis en goroutine for eksempel går i ventetilstand som ved et systemkald, vil Go runtime eksekvere en anden goroutine på OS-tråden, hvilket sikrer, at OS-tråden ikke hviler, men i stedet udnytter CPU'en effektivt, hvilket muliggør hurtig behandling.

Dette er grunden til, at Golang kan opnå højere ydeevne end andre sprog, især ved I/O-opgaver.

De er kortfattede (concise)

En stor fordel er også, at man let kan håndtere en funktion med blot nøgleordet go, når der er behov for samtidighed.

Komplekse låse såsom Mutex og Semaphore skal anvendes, og når låse bruges, er man tvunget til at overveje Deadlock-tilstanden, hvilket uundgåeligt kræver komplekse trin allerede i designfasen før udviklingen.

Goroutine anbefaler dataoverførsel via Channel i overensstemmelse med filosofien om "kommuniker ved at dele hukommelse, del ikke hukommelse for at kommunikere". SELECT understøtter også funktionen, der, kombineret med Channel, gør det muligt at behandle data fra den kanal, hvor data er klar. Desuden kan man ved hjælp af sync.WaitGroup nemt vente, indtil alle goroutiner er færdige, hvilket gør det let at styre arbejdsflowet. Takket være disse værktøjer kan datakonkurrenceproblemer mellem tråde undgås, og samtidighed kan håndteres mere sikkert.

Derudover kan man ved hjælp af context styre livscyklus, annullering, timeout, deadline og anmodningsomfang på brugerniveau (User-Level), hvilket sikrer en vis grad af stabilitet.

Goroutine's parallelle opgaver (GOMAXPROCS)

Jeg har talt om, hvor god goroutines samtidighed er, men du undrer dig måske over, om parallelisme ikke understøttes. Antallet af Cores i nutidens CPU'er overstiger ofte tocifrede tal i modsætning til tidligere, og selv husholdnings-pc'er har et betydeligt antal Cores.

Men Goroutine udfører også parallelle opgaver, og det er via GOMAXPROCS.

Hvis GOMAXPROCS ikke er indstillet, varierer indstillingen afhængigt af versionen.

  1. Før 1.5: Standardværdien er 1. Hvis der er behov for mere end 1, er det essentielt at indstille det, f.eks. med runtime.GOMAXPOCS(runtime.NumCPU()).

  2. 1.5 til 1.24: Ændret til at bruge alle tilgængelige logiske Cores. Fra dette tidspunkt er det ikke nødvendigt for udviklere at indstille det, medmindre der er særlige begrænsninger.

  3. 1.25: Som et sprog der er populært i container-miljøer, kontrolleres cGroup i Linux for at verificere CPU-begrænsningen indstillet for containeren.

    Hvis antallet af logiske Cores er 10, og CPU-begrænsningen er 5, indstilles GOMAXPROCS til det lavere tal, 5.

Revisionen i 1.25 udgør en meget stor ændring. Det skyldes, at sprogets anvendelighed i container-miljøer er steget. Dette har muliggjort forebyggelse af CPU-throttling ved at reducere unødvendig trådoprettelse og kontekstskift.

 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: Starter\n", name) // Goroutine %d: starter
14	time.Sleep(10 * time.Millisecond) // Forsinkelse for at simulere arbejde
15	fmt.Printf("Goroutine %d: Afslutter\n", name) // Goroutine %d: afslutter
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // Bruger kun 2 CPU Cores
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("Venter på, at alle goroutiner er færdige...") // Venter på, at alle goroutiner er færdige...
29	wg.Wait()
30	fmt.Println("Alle opgaver er fuldført.") // Alle opgaver er fuldført.
31
32}
33

Goroutine's Scheduler (M:N-model)

I den tidligere del om, at goroutine administreres ved at blive tildelt OS-tråde ved hjælp af M:N-modellen, kan vi gå lidt mere i detaljer med goroutine GMP-modellen.

  • G (Goroutine): Den mindste arbejdsenhed, der udføres i Go
  • M (Machine): OS-tråd (den faktiske arbejdsplacering)
  • P (Processor): Den logiske processor, som Go runtime administrerer

P har desuden en lokal eksekveringskø (Local Run Queue) og fungerer som en scheduler, der tildeler tildelte G'er til M. Kort sagt er goroutine

GMP's funktionsmåde er som følger:

  1. Når en G (Goroutine) oprettes, tildeles den til P's (Processor) lokale eksekveringskø.
  2. P (Processor) tildeler G (Goroutine) i den lokale eksekveringskø til M (Machine).
  3. M (Machine) returnerer G's (Goroutine) tilstand: block, complete, preempted.
  4. Work-Stealing (Arbejdstyveri): Hvis P's lokale eksekveringskø bliver tom, kontrollerer en anden P den globale kø. Hvis der heller ikke er nogen G (Goroutine) der, stjæler den arbejde fra en anden lokal P (Processor) for at sikre, at alle M'er forbliver aktive.
  5. Systemkaldshåndtering (Blocking): Hvis G (Goroutine) blokeres under udførelsen, går M (Machine) i ventetilstand. På dette tidspunkt adskilles P (Processor) fra den blokerede M (Machine) og kombineres med en anden M (Machine) for at udføre den næste G (Goroutine). Dette sikrer, at der ikke spildes CPU-ressourcer under ventetiden, selv under I/O-opgaver.
  6. Hvis en G (Goroutine) præempteres (preempted) i lang tid, gives eksekveringsmuligheden til en anden G (Goroutine).

Golangs GC (Garbage Collector) kører også på Goroutine, hvilket minimerer afbrydelsen af applikationens eksekvering (STW) og muliggør parallel hukommelsesrydning, hvilket udnytter systemressourcerne effektivt.

Afslutningsvis er Golang en af sprogets stærke fordele, og da der er mange flere, håber jeg, at mange udviklere vil nyde at bruge Go.

Tak.