Gorutinens grundlæggende principper
Goroutine
Når man beder Gopher-udviklere om at forklare fordelene ved Golang, dukker der ofte artikler op om Concurrency. Grundlaget for dette er goroutines, som er lette og nemme at håndtere. Jeg har udarbejdet en kort tekst om dette.
Concurrency vs. Parallelism
Før vi forstår goroutines, vil jeg gerne forklare to begreber, der ofte forveksles.
- Concurrency: Concurrency handler om at behandle mange opgaver på én gang. Det betyder ikke nødvendigvis, at de udføres samtidigt; det 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. Concurrency er muligt selv på en single-core processor.
- Parallelism: Parallelism er "behandling af flere opgaver samtidigt på flere cores". Det betyder bogstaveligt talt at udføre opgaver parallelt og køre forskellige opgaver samtidigt.
Goroutines gør det nemt at implementere concurrency via Go runtime scheduler, og udnytter naturligt parallelism gennem GOMAXPROCS
indstillingen.
Javas multi-threading, som ofte har høj udnyttelsesgrad, er et typisk eksempel på parallelism.
Hvorfor er goroutines gode?
lightweight
Oprettelsesomkostningerne er meget lave sammenlignet med andre sprog. Spørgsmålet opstår, hvorfor Golang bruger færre ressourcer? Dette skyldes, at oprettelsesstedet styres internt i Go runtime. Årsagen er, at det er en letvægts logisk tråd, der er mindre end en OS-tråd. Den kræver en initial stak på omkring 2KB, og stakken kan dynamisk udvides afhængigt af brugerens implementering.
Ved at administrere i stak-enheder er oprettelse og fjernelse meget hurtig og billig, hvilket gør det muligt at køre millioner af goroutines uden at det bliver en byrde. Som et resultat minimerer Goroutine OS-kernelindgreb takket være runtime scheduler.
performance
For det første, som forklaret ovenfor, har Goroutine mindre OS-kernelindgreb, hvilket gør kontekstskift på User-Level billigere og hurtigere end på OS-trådniveau.
Derudover allokeres og administreres de til OS-tråde ved hjælp af en M:N-model. Ved at oprette en OS-trådpool er det muligt at behandle opgaver med færre tråde, uden behov for mange tråde. Hvis en Goroutine for eksempel går i ventetilstand, såsom et systemkald, vil Go runtime køre en anden Goroutine på OS-tråden, hvilket betyder, at OS-tråden ikke hviler, men effektivt udnytter CPU'en for hurtig behandling.
Dette gør det muligt for Golang at opnå højere performance end andre sprog, især ved I/O-operationer.
concise
En stor fordel er også, at hvis concurrency er nødvendig, kan en funktion nemt behandles med et enkelt go
-nøgleord.
Man skal bruge komplekse Locks såsom Mutex
og Semaphore
, og hvis man bruger Locks, er man tvunget til at overveje Deadlock-tilstande, hvilket kræver komplekse trin allerede i designfasen før udvikling.
Goroutine anbefaler datatransmission via Channel
i henhold til filosofien "kommuniker for at dele hukommelse, del ikke hukommelse ved at kommunikere", og SELECT
understøtter endda funktionen til at behandle data fra den kanal, der er klar, når den kombineres med Channel
. Desuden, ved at bruge sync.WaitGroup
, kan man nemt vente, indtil alle goroutines er færdige, hvilket forenkler styringen af arbejdsflowet. Disse værktøjer hjælper med at forhindre datakonkurrenceproblemer mellem tråde og muliggør sikrere concurrency-behandling.
Desuden kan kontekst (context) bruges til at styre livscyklus, annullering, timeout, deadline og anmodningsomfang på brugerniveau (User-Level), hvilket sikrer en vis grad af stabilitet.
Goroutine's parallelle opgaver (GOMAXPROCS)
Du undrer dig måske over, at selvom concurrency er en stor fordel ved goroutine, understøtter den så ikke parallelism? Årsagen er, at antallet af cores i moderne CPU'er nu er tocifret, og selv hjemme-pc'er har et betydeligt antal cores.
Men Goroutine udfører også parallelle opgaver, og det er GOMAXPROCS
.
Hvis GOMAXPROCS
ikke er indstillet, indstilles det forskelligt afhængigt af versionen.
Før 1.5: Standardværdien er 1. Hvis mere end 1 er nødvendigt, er det obligatorisk at indstille det, f.eks. med
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 ~ 1.24: Ændret til at bruge alle tilgængelige logiske cores. Fra dette tidspunkt behøver udviklere ikke at indstille det, medmindre der er specifikke begrænsninger.
1.25: Som et populært sprog i container-miljøer kontrolleres cGroup i Linux for at bestemme
CPU-begrænsningen
, der er indstillet for containeren.Hvis antallet af logiske cores er 10, og CPU-begrænsningsværdien er 5, indstilles
GOMAXPROCS
til det lavere antal, nemlig 5.
Revisionen i 1.25 udgør en meget betydelig ændring. Det skyldes, at sprogets anvendelighed i container-miljøer er steget. Dette har gjort det muligt at reducere unødvendig trådoprettelse og kontekstskift, 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: 시작\n", name) // Goroutine %d: Start
14 time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 (Forsinkelse til arbejdssimulering)
15 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Start
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 (Brug 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("모든 goroutine이 끝날 때까지 대기합니다...") // Venter på, at alle goroutines er færdige...
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.") // Alle opgaver er fuldført.
31
32}
33
Goroutine's Scheduler (M:N-model)
Hvis vi går mere specifikt ind i den tidligere diskussion om M:N-modellen, der bruges til at allokere og administrere OS-tråde, finder vi Goroutine GMP-modellen.
- G (Goroutine): Den mindste arbejdsenhed, der udføres i Go.
- M (Machine): OS-tråd (faktisk arbejdssted).
- P (Processor): En logisk processor, der styres af Go runtime.
P har desuden en lokal eksekveringskø (Local Run Queue) og fungerer som en scheduler, der tildeler tildelte G'er til M'er. Enkelt sagt er en goroutine
GMP-processen er som følger:
- Når en G (Goroutine) oprettes, tildeles den til P's (Processor) lokale eksekveringskø.
- P (Processor) tildeler G (Goroutine) fra den lokale eksekveringskø til M (Machine).
- M (Machine) returnerer G's (Goroutine) status: block, complete, preempted.
- Work-Stealing: 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 konstant er i drift.
- Håndtering af systemkald (Blocking): Hvis en G (Goroutine) blokerer under udførelse, går M (Machine) i ventetilstand. I dette tilfælde adskiller P (Processor) den blokerede M (Machine) og forbinder den med en anden M (Machine) for at udføre den næste G (Goroutine). Under denne proces er der intet spild af CPU under ventetiden for I/O-operationer.
- Hvis en G (Goroutine) optager (preempted) lang tid, gives udførelsesmuligheden til en anden G (Goroutine).
Golang's GC (Garbage Collector) kører også på Goroutine, hvilket minimerer afbrydelser (STW) af applikationens udførelse og muliggør parallel hukommelsesrydning, 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.