Grundlæggende om Gorutiner
Goroutine
Når man beder Gophers om at tale om fordelene ved golang, er der ofte en artikel, der omhandler Concurrency. Grundlaget for dette indhold er de lette og nemt håndterbare goroutines. Jeg har skrevet en kort oversigt herom.
Concurrency vs. Parallelism
Før vi forstår goroutines, vil jeg først adressere to ofte forvekslede begreber.
- Concurrency: Concurrency handler om at håndtere mange opgaver på én gang. Det betyder ikke nødvendigvis, at de udføres samtidigt; det er 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. Concurrency er muligt selv på en single-core processor.
- Parallelism: Parallelism er "at behandle flere opgaver samtidigt på flere kerner". Det betyder bogstaveligt talt at udføre arbejde parallelt og eksekvere forskellige opgaver samtidigt.
Goroutines gør det nemt at implementere concurrency via Go runtime scheduler, og udnytter naturligt parallelism gennem GOMAXPROCS-indstillingen.
Javas ofte anvendte Multi-thread er et typisk eksempel på parallelism.
Hvorfor er goroutines gode?
Lightweight
Oprettelsesomkostningerne er meget lavere sammenlignet med andre sprog. Her opstår spørgsmålet, hvorfor golang bruger færre ressourcer? Det skyldes, at oprettelsesstedet styres internt i Go runtime. Dette skyldes, at det er en letvægts logisk tråd, der er mindre end en OS-tråd, og den indledende stak kræver cirka 2KB plads og kan dynamisk variere ved at tilføje stak afhængigt af brugerens implementering.
Ved at administrere i stakenheder er oprettelse og fjernelse meget hurtig og billig, hvilket muliggør problemfri håndtering af millioner af goroutines. På grund af dette kan Goroutine minimere OS-kernens indgriben takket være runtime-scheduler.
God performance
Først og fremmest, som beskrevet ovenfor, har Goroutine mindre OS-kerneindgriben, hvilket gør kontekstskifte på User-Level billigere og hurtigere end på OS-trådniveau.
Derudover allokeres og administreres de til OS-tråde ved hjælp af M:N-modellen. Ved at oprette en OS-trådpulje er det muligt at behandle med få tråde uden at have brug for mange. For eksempel, hvis den går i en ventetilstand, såsom et systemkald, vil Go-runtime køre en anden goroutine på OS-tråden, så OS-tråden ikke hviler og effektivt udnytter CPU'en til hurtig behandling.
Dette gør, at Golang kan opnå højere performance i I/O-operationer sammenlignet med andre sprog.
Concise
En stor fordel er også, at hvis concurrency er nødvendigt, kan en funktion nemt behandles med et enkelt go-nøgleord.
Man skal bruge komplekse Locks som Mutex og Semaphore, og når man bruger Locks, er man uundgåeligt nødt til at overveje Deadlock-tilstande, hvilket kræver en kompleks fase allerede fra designstadiet før udvikling.
Goroutine fremmer dataoverførsel via Channel i overensstemmelse med filosofien "del ikke hukommelse ved at kommunikere, men kommuniker for at dele hukommelse", og SELECT understøtter endda funktionen til at behandle data fra den kanal, der er klar, når den kombineres med Channel. Desuden kan man med sync.WaitGroup nemt vente, indtil alle goroutines er afsluttet, hvilket gør arbejdsflowet nemt at administrere. Takket være disse værktøjer undgås datakonkurrenceproblemer mellem tråde, og concurrency-behandling kan udføres mere sikkert.
Desuden kan man ved hjælp af context styre livscyklus, annullering, timeout, deadline og anmodningsomfang på User-Level, hvilket sikrer en vis grad af stabilitet.
Goroutines parallelle opgaver (GOMAXPROCS)
Jeg har nævnt de gode aspekter ved goroutines' concurrency, men du undrer dig måske over, om parallelism ikke understøttes. Antallet af CPU-kerner i dag er anderledes end tidligere, og overstiger ofte tocifrede tal, og selv hjemme-pc'er har et betydeligt antal kerner.
Men Goroutine udfører også parallelle opgaver, og det er GOMAXPROCS.
Hvis GOMAXPROCS ikke er indstillet, konfigureres det forskelligt afhængigt af versionen.
Før 1.5: Standardværdi 1, hvis mere end 1 var nødvendig, var det obligatorisk at indstille det som f.eks.
runtime.GOMAXPOCS(runtime.NumCPU()).1.5 ~ 1.24: Ændret til alle tilgængelige logiske kerner. Fra dette tidspunkt var det ikke nødvendigt for udviklere at indstille det, medmindre der var særlige begrænsninger.
1.25: Som et sprog kendt i container-miljøer kontrollerer det cGroup på Linux for at finde
CPU-begrænsningen, der er indstillet for containeren.Hvis antallet af logiske kerner er 10, og CPU-begrænsningsværdien er 5, vil
GOMAXPROCSblive indstillet til det lavere tal, 5.
Revisionen i 1.25 udgør en meget stor ændring. Det skyldes, at sprogudnyttelsen i container-miljøer er steget. Dette har gjort det muligt at reducere unødvendig trådoprettelse og kontekstskifte, hvilket forhindrer CPU-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: starter\n", name) // Goroutine %d: starter
14 time.Sleep(10 * time.Millisecond) // Forsinkelse for at simulere arbejde
15 fmt.Printf("Goroutine %d: færdig\n", name) // Goroutine %d: færdig
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // Brug kun 2 CPU-kerner
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 goroutines er færdige...") // Venter på, at alle goroutines er færdige...
29 wg.Wait()
30 fmt.Println("Alle opgaver er afsluttet.") // Alle opgaver er afsluttet.
31
32}
33
Goroutines Scheduler (M:N-model)
Med hensyn til den tidligere nævnte del om allokering og administration til OS-tråde ved hjælp af M:N-modellen, er der mere specifikt goroutine GMP-modellen.
- G (Goroutine): Den mindste arbejdsenhed, der udføres i Go
- M (Machine): OS-tråd (faktisk arbejdssted)
- P (Processor): Den logiske processor, der administreres af Go runtime
P har yderligere en Local Run Queue og fungerer som en scheduler, der tildeler tildelte G'er til M. Kort sagt er goroutine
GMP-operationsprocessen er som følger:
- Når en G (Goroutine) oprettes, tildeles den til P's (Processor) Local Run Queue.
- P (Processor) tildeler G (Goroutine) i Local Run Queue til M (Machine).
- M (Machine) returnerer G's (Goroutine) status: block, complete, preempted.
- Work-Stealing: Hvis P's Local Run Queue 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 arbejder uafbrudt.
- System Call Handling (Blocking): Hvis en G (Goroutine) blokeres under udførelse, går M (Machine) i en ventetilstand. På dette tidspunkt adskiller P (Processor) sig fra den blokerede M (Machine) og kombinerer sig med en anden M (Machine) for at udføre den næste G (Goroutine). I denne proces er der ingen CPU-spild selv under ventetid for I/O-operationer.
- Hvis en G (Goroutine) beslaglægger (preempted) i lang tid, gives udførelsesmuligheden til en anden G (Goroutine).
Golang's GC (Garbage Collector) kører også på Goroutine, hvilket minimerer afbrydelser i applikationens udførelse (STW) og rydder op i hukommelsen parallelt, hvilket effektivt udnytter systemressourcer.
Endelig er Golang en af sprogets stærke fordele, og der er mange flere. Jeg håber, at mange udviklere vil nyde at bruge Golang.
Tak.