GoSuda

Goroutiners grunnleggende prinsipper

By hamori
views ...

Goroutine

Når man ber Gopher-e forklare fordelene med Golang, kommer ofte artikler om samtidighet (Concurrency) opp. Grunnlaget for dette er den lette og enkle håndteringen av goroutine. Jeg har skrevet en kortfattet oversikt over dette.

Concurrency vs. Parallelism

Før vi forstår goroutine, vil jeg først klargjøre to ofte forvekslede begreper.

  • Concurrency: Concurrency handler om å håndtere mange oppgaver samtidig. Det betyr ikke nødvendigvis at de utføres samtidig i praksis, men er et strukturelt og logisk konsept der flere oppgaver deles inn i mindre enheter og utføres vekselvis, slik at det for brukeren ser ut som om flere oppgaver behandles samtidig. Concurrency er mulig selv med en enkelt kjerne.
  • Parallelism: Parallelism er "å behandle flere oppgaver samtidig på flere kjerner". Det betyr bokstavelig talt å utføre arbeid parallelt, der ulike oppgaver kjøres samtidig.

Goroutine gjør det enkelt å implementere concurrency gjennom Go runtime scheduler, og utnytter også parallelism naturlig via GOMAXPROCS-innstillingen.

Javas multi-threading, som ofte har høy utnyttelsesgrad, er et typisk eksempel på parallelism.

Hvorfor er goroutine så bra?

Lettvekt (lightweight)

Kostnaden ved å opprette dem er svært lav sammenlignet med andre språk. Spørsmålet oppstår da hvorfor Golang bruker så lite her? Svaret er at opprettelsen administreres internt i Go runtime. Dette skyldes at det er en lettvektig logisk tråd, mindre enn en OS-tråd, og den initiale stacken krever ca. 2KB plass og kan dynamisk endres ved å legge til stack i henhold til brukerens implementasjon.

Ved å administrere stackenheter blir opprettelse og fjerning svært raskt og kostnadseffektivt, noe som gjør det mulig å kjøre millioner av goroutiner uten betydelig belastning. Som et resultat kan Goroutine minimere OS-kjerneinngrep takket være runtime scheduler.

God ytelse (performance)

For det første, som forklart ovenfor, innebærer Goroutine minimal OS-kjerneinngrep. Ved kontekstbytte på brukernivå (User-Level) er kostnaden lavere enn for OS-tråder, noe som muliggjør rask bytte mellom oppgaver.

I tillegg brukes M:N-modellen for å allokere og administrere OS-tråder. Ved å opprette en OS-trådpulje er det mulig å håndtere oppgaver med færre tråder, uten behov for mange tråder. For eksempel, hvis en Goroutine går inn i en ventetilstand, som et systemkall, vil Go runtime kjøre en annen goroutine på OS-tråden, slik at OS-tråden ikke hviler og CPU-en utnyttes effektivt for rask behandling.

Dette gjør at Golang kan oppnå høyere ytelse i I/O-operasjoner sammenlignet med andre språk.

Konsis (concise)

En stor fordel er også at man enkelt kan håndtere funksjoner med ett enkelt go-nøkkelord når concurrency er nødvendig.

Man må bruke komplekse Locks som Mutex og Semaphore, og ved bruk av Locks er det uunngåelig å vurdere Deadlock-tilstander, noe som krever komplekse trinn allerede i designfasen før utvikling.

Goroutine anbefaler dataoverføring via Channel i tråd med filosofien "Ikke kommuniser ved å dele minne, men del minne ved å kommunisere". SELECT støtter også funksjonalitet som, i kombinasjon med Channel, gjør det mulig å behandle kanaler der data er klare først. I tillegg, ved å bruke sync.WaitGroup, kan man enkelt vente til alle goroutine har fullført, noe som forenkler administrasjonen av arbeidsflyten. Takket være disse verktøyene kan man forhindre problemer med datakonkurranse mellom tråder og håndtere concurrency på en sikrere måte.

Videre kan man, ved å bruke context, kontrollere livssyklus, kansellering, timeout, deadline og omfang av forespørsler på brukernivå (User-Level), noe som sikrer en viss grad av stabilitet.

Parallell utførelse av Goroutine (GOMAXPROCS)

Selv om vi har snakket om fordelene med concurrency i goroutine, lurer du kanskje på om parallelism ikke støttes. Antall kjerner i dagens CPU-er er over tosifret, i motsetning til tidligere, og selv hjemme-PC-er har et betydelig antall kjerner.

Men Goroutine utfører også parallell arbeid; det er GOMAXPROCS.

Hvis GOMAXPROCS ikke er satt, konfigureres det forskjellig avhengig av versjonen.

  1. Før 1.5: Standardverdi 1. Hvis mer enn 1 er nødvendig, er det obligatorisk å sette det med runtime.GOMAXPOCS(runtime.NumCPU()) eller lignende.

  2. 1.5 ~ 1.24: Endret til antall tilgjengelige logiske kjerner. Fra dette tidspunktet er det ikke nødvendig for utviklere å sette det, med mindre det er spesifikke begrensninger.

  3. 1.25: Som et populært språk i container-miljøer, sjekker det cGroup på Linux for å bekrefte CPU-begrensningen satt for containeren.

    Hvis antall logiske kjerner er 10 og CPU-begrensningen er 5, vil GOMAXPROCS bli satt til det lavere tallet 5.

Endringen i 1.25 er en svært betydelig endring. Dette skyldes at språket har blitt mer anvendelig i container-miljøer. Som et resultat har det blitt mulig å redusere unødvendig trådgenerering og kontekstbytte, og dermed forhindre 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: start\n", name) // Start av Goroutine
14	time.Sleep(10 * time.Millisecond) // Forsinkelse for å simulere arbeid
15	fmt.Printf("Goroutine %d: start\n", name) // Start av Goroutine
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // Bruk kun 2 CPU-kjerner
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 til alle goroutine er ferdige...") // Venter på ferdigstillelse av Goroutine
29	wg.Wait()
30	fmt.Println("Alle oppgaver er fullført.") // Alle oppgaver er fullført
31
32}
33

Goroutine Scheduler (M:N-modellen)

Fra den tidligere nevnte delen om M:N-modellen, som allokerer og administrerer OS-tråder, vil vi nå gå litt mer detaljert inn på goroutine GMP-modellen.

  • G (Goroutine): Den minste arbeidsenheten som kjøres i Go.
  • M (Machine): OS-tråd (faktisk arbeidssted).
  • P (Processor): En logisk prosess som administreres av Go runtime.

P har i tillegg en lokal kjøringskø (Local Run Queue) og fungerer som en scheduler som tildeler G-er til M-er. Kort sagt, goroutine er

GMP-operasjonsprosessen er som følger:

  1. Når en G (Goroutine) er opprettet, tildeles den til P-ens (Processor) lokale kjøringskø.
  2. P (Processor) tildeler G-en (Goroutine) fra den lokale kjøringskøen til M-en (Machine).
  3. M (Machine) returnerer G-ens (Goroutine) tilstand: block, complete, preempted.
  4. Work-Stealing (arbeidstyveri): Hvis P-ens lokale kjøringskø blir tom, sjekker en annen P den globale køen. Hvis det heller ikke er G-er (Goroutine) der, stjeler den arbeid fra en annen lokal P (Processor) for å sikre at alle M-er fortsetter å fungere.
  5. Systemkallbehandling (Blocking): Hvis en G (Goroutine) blokkeres under utførelse, går M-en (Machine) i ventemodus. I dette tilfellet kobler P-en (Processor) seg fra den blokkerte M-en (Machine) og kobler seg til en annen M (Machine) for å kjøre den neste G-en (Goroutine). Dette forhindrer CPU-sløsing selv under ventetid for I/O-operasjoner.
  6. Hvis en G (Goroutine) okkuperer prosessoren for lenge (preempted), gis utførelsesmuligheten til en annen G (Goroutine).

Golang's GC (Garbage Collector) kjører også på Goroutine, noe som minimerer avbrudd i applikasjonens utførelse (STW) og tillater parallell minneopprydding, og dermed effektiv ressursutnyttelse.

Til slutt er Golang en av språkets sterke sider, og det er mange flere. Jeg håper mange utviklere vil nyte Go.

Takk.