GoSuda

Goroutiner: Grunnleggende prinsipper

By hamori
views ...

Goroutine

Når man snakker med Gophere om fordelene med Golang, er det ofte en diskusjon om samtidighet (Concurrency). Grunnlaget for dette er goroutine, som er lettvekts og enkel å håndtere. Jeg har skrevet en kort oversikt over dette.

Samtidighet (Concurrency) vs. Parallellitet (Parallelism)

Før vi forstår goroutine, vil jeg kort forklare to begreper som ofte forveksles.

  • Samtidighet: Samtidighet handler om å håndtere mange oppgaver samtidig. Det betyr ikke nødvendigvis at de faktisk utføres samtidig, men det er et strukturelt og logisk konsept som får det til å se ut som om flere oppgaver behandles samtidig ved å dele dem inn i mindre enheter og vekselvis utføre dem. Samtidighet er mulig selv med en enkelt kjerne.
  • Parallellitet: Parallellitet er "å håndtere flere oppgaver samtidig på flere kjerner". Det betyr bokstavelig talt å utføre arbeid parallelt og kjøre forskjellige oppgaver samtidig.

Goroutine gjør det enkelt å implementere samtidighet via Go-runtime-skeduleren, og utnytter parallellitet naturlig gjennom GOMAXPROCS-innstillingen.

Javas Multi-thread, som ofte har høy utnyttelse, er et typisk eksempel på parallellitet.

Hvorfor er Goroutine bra?

Lettvekt (lightweight)

Kostnaden for å opprette goroutiner er svært lav sammenlignet med andre språk. Man kan spørre seg hvorfor Golang bruker færre ressurser; dette skyldes at opprettelsen administreres internt av Go-runtime. Dette er fordi det er en lettvekts logisk tråd, mindre enn en OS-trådenhet, og den krever en initial stakkstørrelse på omtrent 2 KB. Stakken kan dynamisk skaleres ved å legge til mer minne avhengig av brukerens implementasjon.

Ved å administrere stakkenheter er opprettelse og sletting 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-kjerneintervensjon takket være runtime-skeduleren.

God ytelse (performance)

For det første, som nevnt ovenfor, krever Goroutine mindre OS-kjerneintervensjon, og ved kontekstbytte på brukernivå (User-Level) er kostnadene lavere enn for OS-trådenheter, noe som muliggjør raskere oppgaveveksling.

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

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

Konsis (concise)

En stor fordel er også at man enkelt kan behandle funksjoner med nøkkelordet go når samtidighet er nødvendig.

Man må bruke komplekse låser som Mutex, Semaphore, og når låser brukes, er man tvunget til å vurdere Deadlock-tilstander, noe som krever komplekse stadier 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 gjør det mulig å behandle data fra den kanalen som er klar, i kombinasjon med Channel. I tillegg, ved å bruke sync.WaitGroup, kan man enkelt vente til alle goroutiner er ferdige, noe som gjør det enkelt å administrere arbeidsflyten. Takket være disse verktøyene kan man unngå datakonkurranseproblemer mellom tråder og behandle samtidighet på en sikrere måte.

I tillegg kan kontekst (context) brukes til å kontrollere livssyklus, kansellering, tidsavbrudd, frister og forespørselsomfang på brukernivå (User-Level), noe som sikrer en viss grad av stabilitet.

Goroutines parallelle arbeid (GOMAXPROCS)

Vi har snakket om fordelene med goroutines samtidighet, men du lurer kanskje på om den ikke støtter parallellitet. Antall kjerner i moderne CPUer er nå over tosifret, og selv hjemme-PC-er har et betydelig antall kjerner.

Goroutine utfører imidlertid også parallelt arbeid, og det er GOMAXPROCS.

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

  1. Før 1.5: Standardverdi 1, må settes med runtime.GOMAXPOCS(runtime.NumCPU()) hvis mer enn 1 er nødvendig.

  2. 1.5 ~ 1.24: Endret til alle tilgjengelige logiske kjerner. Fra dette tidspunktet trengte utviklere ikke å sette det med mindre det var 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 settes til den lavere verdien 5.

Endringen i 1.25 er en svært betydelig endring. Dette skyldes at språkets anvendelighet i container-miljøer har økt. Som et resultat er det nå mulig å redusere unødvendig trådopprettelse 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: starter\n", name) // Goroutine %d: starter
14	time.Sleep(10 * time.Millisecond) // Forsinkelse for å simulere arbeid
15	fmt.Printf("Goroutine %d: ferdig\n", name) // Goroutine %d: ferdig
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 på at alle goroutiner skal fullføres...") // Venter på at alle goroutiner skal fullføres...
29	wg.Wait()
30	fmt.Println("Alle oppgaver er fullført.") // Alle oppgaver er fullført.
31
32}
33

Goroutines Scheduler (M:N-modell)

Fra det tidligere nevnte innholdet om at M:N-modellen brukes til å allokere og administrere OS-tråder, vil vi nå se mer spesifikt på goroutine GMP-modellen.

  • G (Goroutine): Den minste arbeidsenheten som kjører 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 utførelseskø (Local Run Queue) og fungerer som en skedulerer som tildeler tildelte G-er til M-er. Enkelt sagt, goroutine:

GMPs operasjonsprosess er som følger:

  1. Når en G (Goroutine) er opprettet, tildeles den til P's (Processor) lokale utførelseskø.
  2. P (Processor) tildeler G (Goroutine) fra sin lokale utførelseskø til M (Machine).
  3. M (Machine) returnerer G's (Goroutine) status: block, complete, preempted.
  4. Work-Stealing: Hvis P's lokale utførelseskø 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 å arbeide.
  5. Systemkallbehandling (Blocking): Hvis en G (Goroutine) blokkeres under utførelse, går M (Machine) inn i en ventetilstand. På dette tidspunktet kobler P (Processor) seg fra den blokkerte M (Machine) og kobler seg til en annen M (Machine) for å utføre neste G (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 andre G-er (Goroutine).

Golang's GC (Garbage Collector) kjører også på Goroutine, noe som gjør det mulig å rydde opp i minnet parallelt med minimal avbrudd i applikasjonens utførelse (STW), og dermed utnytte systemressursene effektivt.

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

Takk.