Go Samtidighets-starterpakke
Oversikt
Kort introduksjon
Go-språket har mange verktøy for samtidighetshåndtering. I denne artikkelen vil vi introdusere noen av dem og noen "triks".
Goroutine?
En goroutine er en ny form for samtidighet-modell støttet av Go-språket. Vanligvis mottar et program OS-tråder fra operativsystemet for å utføre flere oppgaver samtidig, og utfører oppgaver parallelt opp til antall kjerner. For å utføre samtidighet i mindre enheter, opprettes grønne tråder i userland, slik at flere grønne tråder kjører innenfor en enkelt OS-tråd for å utføre oppgaver. Imidlertid har goroutiner gjort denne typen grønne tråder enda mindre og mer effektive. Disse goroutinene bruker mindre minne enn tråder, og kan opprettes og byttes ut raskere enn tråder.
For å bruke en goroutine er det bare å bruke go-nøkkelordet. Dette gjør det mulig å intuitivt utføre synkron kode asynkront under programmeringsprosessen.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch := make(chan struct{})
10 go func() {
11 defer close(ch) // Lukk channelen når goroutinen er ferdig
12 time.Sleep(1 * time.Second) // Vent i 1 sekund
13 fmt.Println("Hello, World!") // Skriv ut "Hello, World!"
14 }()
15
16 fmt.Println("Waiting for goroutine...") // Skriv ut "Waiting for goroutine..."
17 for range ch {} // Vent til channelen er lukket
18}
Denne koden endrer enkelt synkron kode som venter i 1 sekund og deretter skriver ut Hello, World! til en asynkron flyt. Selv om eksemplet er enkelt, vil endring av litt mer kompleks kode fra synkron til asynkron forbedre lesbarheten, synligheten og forståelsen av koden sammenlignet med eksisterende metoder som async await eller promise.
Imidlertid, i mange tilfeller, kan dårlig goroutine-kode oppstå hvis man ikke forstår flyten av å kalle synkron kode asynkront og flyten av fork & join (ligner på "divide and conquer"-flyten). I denne artikkelen vil vi introdusere noen metoder og teknikker for å forberede seg på slike tilfeller.
Samtidighetshåndtering
Context
Det kan være overraskende at context dukker opp som den første styringsteknikken. Men i Go-språket går context utover enkel kanselleringsfunksjonalitet og spiller en utmerket rolle i å styre hele oppgavetreet. For de som ikke er kjent med det, vil jeg kort forklare denne pakken.
1package main
2
3import (
4 "context"
5 "fmt"
6 "time"
7)
8
9func main() {
10 ctx, cancel := context.WithCancel(context.Background()) // Opprett en context med kanselleringsfunksjon
11 defer cancel() // Sørg for at kanselleringsfunksjonen kalles når main-funksjonen avsluttes
12
13 go func() {
14 <-ctx.Done() // Vent til contexten er kansellert
15 fmt.Println("Context is done!") // Skriv ut "Context is done!"
16 }()
17
18 time.Sleep(1 * time.Second) // Vent i 1 sekund
19
20 cancel() // Kanseller contexten
21
22 time.Sleep(1 * time.Second) // Vent i 1 sekund
23}
Koden ovenfor bruker context til å skrive ut Context is done! etter 1 sekund. context kan sjekke om den er kansellert via Done()-metoden, og tilbyr ulike kanselleringsmetoder via metoder som WithCancel, WithTimeout, WithDeadline og WithValue.
La oss lage et enkelt eksempel. Anta at du skriver kode for å hente user, post og comment ved hjelp av aggregator-mønsteret for å hente data. Og hvis alle forespørsler må fullføres innen 2 sekunder, kan du skrive det slik:
1package main
2
3import (
4 "context"
5 "fmt"
6 "time"
7)
8
9func getUser(ctx context.Context) string {
10 time.Sleep(500 * time.Millisecond) // Simulerer henting av brukerdata
11 return "User"
12}
13
14func getPost(ctx context.Context) string {
15 time.Sleep(500 * time.Millisecond) // Simulerer henting av innleggsdata
16 return "Post"
17}
18
19func getComment(ctx context.Context) string {
20 time.Sleep(500 * time.Millisecond) // Simulerer henting av kommentardata
21 return "Comment"
22}
23
24func main() {
25 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) // Opprett en context med en tidsavbrudd på 2 sekunder
26 defer cancel() // Sørg for at kanselleringsfunksjonen kalles når main-funksjonen avsluttes
27
28 ch := make(chan struct{}) // Opprett en channel for å signalisere at alle data er hentet
29 go func() {
30 defer close(ch) // Lukk channelen når goroutinen er ferdig
31 user := getUser(ctx) // Hent brukerdata
32 post := getPost(ctx) // Hent innleggsdata
33 comment := getComment(ctx) // Hent kommentardata
34
35 fmt.Println(user, post, comment) // Skriv ut de hentede dataene
36 }()
37
38 select {
39 case <-ctx.Done(): // Hvis contexten er ferdig (tidsavbrudd eller kansellert)
40 fmt.Println("Timeout!") // Skriv ut "Timeout!"
41 case <-ch: // Hvis alle data er hentet
42 fmt.Println("All data is fetched!") // Skriv ut "All data is fetched!"
43 }
44}
Koden ovenfor skriver ut Timeout! hvis alle data ikke er hentet innen 2 sekunder, og All data is fetched! hvis alle data er hentet. Ved å bruke context på denne måten kan du enkelt håndtere kansellering og tidsavbrudd, selv i kode der flere goroutiner kjører.
Ulike context-relaterte funksjoner og metoder er tilgjengelige på godoc context. Vi håper du kan lære det grunnleggende og bruke det enkelt.
Channel
Unbuffered channel
channel er et verktøy for kommunikasjon mellom goroutiner. En channel kan opprettes med make(chan T). Her er T typen data som channel skal overføre. Data kan sendes og mottas via <-, og channel kan lukkes med close.
1package main
2
3import "fmt"
4
5func main() {
6 ch := make(chan int) // Opprett en unbuffered channel for int
7 go func() {
8 ch <- 1 // Send 1 til channelen
9 ch <- 2 // Send 2 til channelen
10 close(ch) // Lukk channelen
11 }()
12
13 for i := range ch { // Loop over channelen for å motta verdier
14 fmt.Println(i) // Skriv ut den mottatte verdien
15 }
16}
Koden ovenfor skriver ut 1 og 2 ved hjelp av en channel. Denne koden viser bare sending og mottak av verdier i en channel. Men en channel tilbyr flere funksjoner enn dette. Først skal vi se på buffered channel og unbuffered channel. Før vi begynner, er eksemplet ovenfor en unbuffered channel, der sending av data til channelen og mottak av data fra channelen må skje samtidig. Hvis disse handlingene ikke skjer samtidig, kan det oppstå en deadlock.
Buffered channel
Hva om koden ovenfor ikke bare er enkel utskrift, men to prosesser som utfører tunge oppgaver? Hvis den andre prosessen henger i lang tid mens den leser og behandler, vil den første prosessen også stoppe i samme tidsperiode. Vi kan bruke en buffered channel for å forhindre en slik situasjon.
1package main
2
3import "fmt"
4
5func main() {
6 ch := make(chan int, 2) // Opprett en buffered channel med kapasitet 2
7 go func() {
8 ch <- 1 // Send 1 til channelen
9 ch <- 2 // Send 2 til channelen
10 close(ch) // Lukk channelen
11 }()
12
13 for i := range ch { // Loop over channelen for å motta verdier
14 fmt.Println(i) // Skriv ut den mottatte verdien
15 }
16}
Koden ovenfor skriver ut 1 og 2 ved hjelp av en buffered channel. I denne koden har vi brukt en buffered channel slik at sending av data til channelen og mottak av data fra channelen ikke trenger å skje samtidig. Ved å legge en buffer til channelen får vi en viss kapasitet, noe som kan forhindre forsinkelser forårsaket av avhengige oppgaver.
Select
Når du håndterer flere channels, kan select-setningen enkelt implementere en fan-in-struktur.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch1 := make(chan int, 10) // Opprett buffered channel 1
10 ch2 := make(chan int, 10) // Opprett buffered channel 2
11 ch3 := make(chan int, 10) // Opprett buffered channel 3
12
13 go func() {
14 for {
15 ch1 <- 1 // Send 1 til ch1 hvert sekund
16 time.Sleep(1 * time.Second)
17 }
18 }()
19 go func() {
20 for {
21 ch2 <- 2 // Send 2 til ch2 hvert annet sekund
22 time.Sleep(2 * time.Second)
23 }
24 }()
25 go func() {
26 for {
27 ch3 <- 3 // Send 3 til ch3 hvert tredje sekund
28 time.Sleep(3 * time.Second)
29 }
30 }()
31
32 for i := 0; i < 3; i++ { // Loop 3 ganger for å motta verdier fra channels
33 select {
34 case v := <-ch1: // Motta verdi fra ch1
35 fmt.Println(v)
36 case v := <-ch2: // Motta verdi fra ch2
37 fmt.Println(v)
38 case v := <-ch3: // Motta verdi fra ch3
39 fmt.Println(v)
40 }
41 }
42}
Koden ovenfor oppretter tre channels som periodisk sender 1, 2 og 3, og bruker select til å motta og skrive ut verdier fra channels. Ved å bruke select på denne måten kan du motta data fra flere channels samtidig og behandle dem etter hvert som de mottas.
For range
channel kan enkelt motta data ved hjelp av for range. Når for range brukes på en channel, vil den utføres hver gang data legges til channelen, og loopen avsluttes når channelen lukkes.
1package main
2
3import "fmt"
4
5func main() {
6 ch := make(chan int) // Opprett en channel for int
7 go func() {
8 ch <- 1 // Send 1 til channelen
9 ch <- 2 // Send 2 til channelen
10 close(ch) // Lukk channelen
11 }()
12
13 for i := range ch { // Loop over channelen for å motta verdier
14 fmt.Println(i) // Skriv ut den mottatte verdien
15 }
16}
Koden ovenfor skriver ut 1 og 2 ved hjelp av en channel. I denne koden brukes for range til å motta og skrive ut data hver gang data legges til channelen. Og loopen avsluttes når channelen lukkes.
Som nevnt flere ganger ovenfor, kan denne syntaksen også brukes for enkel synkronisering.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch := make(chan struct{}) // Opprett en channel for synkronisering
10 go func() {
11 defer close(ch) // Lukk channelen når goroutinen er ferdig
12 time.Sleep(1 * time.Second) // Vent i 1 sekund
13 fmt.Println("Hello, World!") // Skriv ut "Hello, World!"
14 }()
15
16 fmt.Println("Waiting for goroutine...") // Skriv ut "Waiting for goroutine..."
17 for range ch {} // Vent til channelen er lukket
18}
Koden ovenfor skriver ut Hello, World! etter å ha ventet i 1 sekund. I denne koden er synkron kode endret til asynkron kode ved hjelp av en channel. Ved å bruke en channel på denne måten kan du enkelt endre synkron kode til asynkron kode og sette join-punkter.
Etc.
- Hvis du sender eller mottar data på en nil channel, kan det oppstå en deadlock ved at den går inn i en uendelig løkke.
- Hvis du sender data etter å ha lukket en channel, vil det oppstå en panic.
- Selv om du ikke eksplisitt lukker en channel, lukkes den av Garbage Collector.
Mutex
Spinlock
En spinlock er en synkroniseringsmetode som kontinuerlig forsøker å låse ved å kjøre en løkke. I Go-språket kan en spinlock enkelt implementeres ved hjelp av pekere.
1package spinlock
2
3import (
4 "runtime"
5 "sync/atomic"
6)
7
8type SpinLock struct {
9 lock uintptr
10}
11
12func (s *SpinLock) Lock() {
13 for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) { // Forsøk å bytte 0 med 1 (lås) atomisk
14 runtime.Gosched() // Gi fra seg CPU-tid
15 }
16}
17
18func (s *SpinLock) Unlock() {
19 atomic.StoreUintptr(&s.lock, 0) // Sett låsen til 0 (lås opp) atomisk
20}
21
22func NewSpinLock() *SpinLock {
23 return &SpinLock{}
24}
Koden ovenfor implementerer spinlock-pakken. I denne koden er SpinLock implementert ved hjelp av sync/atomic-pakken. Lock-metoden forsøker å låse ved å bruke atomic.CompareAndSwapUintptr, og Unlock-metoden frigjør låsen ved å bruke atomic.StoreUintptr. Denne metoden forsøker kontinuerlig å låse uten hvile, noe som betyr at den kontinuerlig bruker CPU-ressurser til låsen er oppnådd, og kan føre til en uendelig løkke. Derfor er det best å bruke spinlock for enkel synkronisering eller når den bare brukes i korte perioder.
sync.Mutex
En mutex er et verktøy for synkronisering mellom goroutiner. mutex levert av sync-pakken tilbyr metoder som Lock, Unlock, RLock og RUnlock. En mutex kan opprettes med sync.Mutex, og en lese-/skrivelås kan også brukes med sync.RWMutex.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 var mu sync.Mutex // Opprett en Mutex
9 var count int // En delt variabel
10
11 go func() {
12 mu.Lock() // Lås Mutexen
13 count++ // Øk count
14 mu.Unlock() // Lås opp Mutexen
15 }()
16
17 mu.Lock() // Lås Mutexen
18 count++ // Øk count
19 mu.Unlock() // Lås opp Mutexen
20
21 println(count) // Skriv ut den endelige verdien av count
22}
I koden ovenfor vil to goroutiner nesten samtidig få tilgang til den samme count-variabelen. Ved å bruke mutex til å gjøre koden som får tilgang til count-variabelen til et kritisk område, kan samtidig tilgang til count-variabelen forhindres. Da vil denne koden alltid skrive ut 2, uansett hvor mange ganger den kjøres.
sync.RWMutex
sync.RWMutex er en mutex som kan brukes med separate lese- og skrivelåser. Lese-låsen kan settes og fjernes ved hjelp av RLock- og RUnlock-metodene.
1package cmap
2
3import (
4 "sync"
5)
6
7type ConcurrentMap[K comparable, V any] struct {
8 sync.RWMutex // Innebygd RWMutex for låsing
9 data map[K]V // Den underliggende map-en
10}
11
12func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
13 m.RLock() // Ta en leselås
14 defer m.RUnlock() // Frigi leselåsen når funksjonen returnerer
15
16 value, ok := m.data[key]
17 return value, ok
18}
19
20func (m *ConcurrentMap[K, V]) Set(key K, value V) {
21 m.Lock() // Ta en skrivelås
22 defer m.Unlock() // Frigi skrivelåsen når funksjonen returnerer
23
24 m.data[key] = value
25}
Koden ovenfor implementerer ConcurrentMap ved hjelp av sync.RWMutex. I denne koden låses en leselås i Get-metoden og en skrivelås i Set-metoden for å trygt få tilgang til og endre data-kartet. Grunnen til at en leselås er nødvendig, er at når det er mange enkle leseoperasjoner, kan flere goroutiner utføre leseoperasjoner samtidig ved å bare sette en leselås uten å sette en skrivelås. Dette kan forbedre ytelsen når en skrivelås ikke er nødvendig fordi det ikke er noen tilstandsendring.
Fakelock
fakelock er et enkelt triks for å implementere sync.Locker. Denne strukturen tilbyr de samme metodene som sync.Mutex, men utfører ingen faktisk handling.
1package fakelock
2
3type FakeLock struct{}
4
5func (f *FakeLock) Lock() {} // Gjør ingenting
6
7func (f *FakeLock) Unlock() {} // Gjør ingenting
Koden ovenfor implementerer fakelock-pakken. Denne pakken implementerer sync.Locker og tilbyr Lock- og Unlock-metoder, men utfører ingen faktiske handlinger. Jeg vil forklare hvorfor slik kode er nødvendig hvis jeg får muligheten.
Waitgroup
sync.WaitGroup
sync.WaitGroup er et verktøy som venter til alle goroutiner er ferdige. Den tilbyr metodene Add, Done og Wait. Add-metoden legger til antall goroutiner, Done-metoden signaliserer at en goroutine er ferdig, og Wait-metoden venter til alle goroutiner er ferdige.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{} // Opprett en WaitGroup
10 c := atomic.Int64{} // En atomisk int64 for trådsikker telling
11
12 for i := 0; i < 100 ; i++ { // Start 100 goroutiner
13 wg.Add(1) // Legg til 1 til WaitGroup-telleren
14 go func() {
15 defer wg.Done() // Signaliser at goroutinen er ferdig når den avsluttes
16 c.Add(1) // Øk teller atomisk
17 }()
18 }
19
20 wg.Wait() // Vent til alle goroutiner er ferdige
21 println(c.Load()) // Skriv ut den endelige verdien av c
22}
Koden ovenfor bruker sync.WaitGroup til å legge til verdier i c-variabelen samtidig av 100 goroutiner. I denne koden brukes sync.WaitGroup til å vente til alle goroutiner er ferdige, og deretter skrives den totale verdien av c ut. Mens channels er tilstrekkelig for å fork & join noen få oppgaver, er sync.WaitGroup et godt valg for å fork & join et stort antall oppgaver.
Med slice
Hvis den brukes sammen med slices, kan waitgroup være et utmerket verktøy for å administrere samtidige kjøringer uten låser.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "math/rand" // Korrigert fra "rand" til "math/rand"
7)
8
9func main() {
10 var wg sync.WaitGroup // Opprett en WaitGroup
11 arr := [10]int{} // Opprett en array med 10 heltall
12
13 for i := 0; i < 10; i++ { // Start 10 goroutiner
14 wg.Add(1) // Legg til 1 til WaitGroup-telleren
15 go func(id int) {
16 defer wg.Done() // Signaliser at goroutinen er ferdig når den avsluttes
17
18 arr[id] = rand.Intn(100) // Generer et tilfeldig tall og lagre det i arrayen
19 }(i)
20 }
21
22 wg.Wait() // Vent til alle goroutiner er ferdige
23 fmt.Println("Done") // Skriv ut "Done"
24
25 for i, v := range arr { // Skriv ut innholdet i arrayen
26 fmt.Printf("arr[%d] = %d\n", i, v)
27 }
28}
Koden ovenfor bruker bare waitgroup for å generere 10 tilfeldige heltall samtidig av hver goroutine og lagre dem i den tildelte indeksen. I denne koden brukes waitgroup til å vente til alle goroutiner er ferdige, og deretter skrives Done ut. Ved å bruke waitgroup på denne måten kan flere goroutiner utføre oppgaver samtidig, lagre data uten låser til alle goroutiner er ferdige, og deretter utføre batch-etterbehandling etter at oppgavene er fullført.
golang.org/x/sync/errgroup.ErrGroup
errgroup er en utvidelse av sync.WaitGroup. I motsetning til sync.WaitGroup, kansellerer errgroup alle goroutiner og returnerer en feil hvis minst én goroutine mislykkes.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/errgroup"
7)
8
9func main() {
10 g, ctx := errgroup.WithContext(context.Background()) // Opprett en errgroup med en context
11 _ = ctx // Ignorer ctx-variabelen da den ikke brukes direkte her
12
13 for i := 0; i < 10; i++ { // Start 10 goroutiner
14 i := i // Opprett en lokal kopi av i
15 g.Go(func() error {
16 if i == 5 { // Hvis i er 5, returner en feil
17 return fmt.Errorf("error")
18 }
19 return nil // Ellers returner nil (ingen feil)
20 })
21 }
22
23 if err := g.Wait(); err != nil { // Vent til alle goroutiner er ferdige og sjekk for feil
24 fmt.Println(err) // Skriv ut feilen hvis den finnes
25 }
26}
Koden ovenfor genererer 10 goroutiner ved hjelp av errgroup og forårsaker en feil i den femte goroutinen. Jeg har med vilje forårsaket en feil i den femte goroutinen for å vise hvordan det ser ut når en feil oppstår. Men i praktisk bruk kan errgroup brukes til å opprette goroutiner og utføre ulike etterbehandlinger for feil som oppstår i hver goroutine.
Once
Et verktøy for å kjøre kode som bare skal kjøres én gang. Relevant kode kan kjøres via konstruktøren nedenfor.
1func OnceFunc(f func()) func()
2func OnceValue[T any](f func() T) func() T
3func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
OnceFunc
OnceFunc sikrer at den gitte funksjonen bare kan utføres én gang gjennom hele programmet.
1package main
2
3import "sync"
4
5func main() {
6 once := sync.OnceFunc(func() { // Opprett en OnceFunc
7 println("Hello, World!") // Funksjonen som skal kjøres én gang
8 })
9
10 once() // Første kall, vil skrive ut "Hello, World!"
11 once() // Andre kall, vil ikke skrive ut noe
12 once() // Tredje kall, vil ikke skrive ut noe
13 once() // Fjerde kall, vil ikke skrive ut noe
14 once() // Femte kall, vil ikke skrive ut noe
15}
Koden ovenfor skriver ut Hello, World! ved hjelp av sync.OnceFunc. I denne koden brukes sync.OnceFunc til å opprette once-funksjonen, og selv om once-funksjonen kalles flere ganger, vil Hello, World! bare skrives ut én gang.
OnceValue
OnceValue utfører ikke bare funksjonen én gang, men lagrer også returverdien fra funksjonen og returnerer den lagrede verdien ved påfølgende kall.
1package main
2
3import "sync"
4
5func main() {
6 c := 0
7 once := sync.OnceValue(func() int { // Opprett en OnceValue
8 c += 1 // Øk c
9 return c // Returner c
10 })
11
12 println(once()) // Første kall, vil returnere og skrive ut 1
13 println(once()) // Andre kall, vil returnere og skrive ut 1 (lagret verdi)
14 println(once()) // Tredje kall, vil returnere og skrive ut 1 (lagret verdi)
15 println(once()) // Fjerde kall, vil returnere og skrive ut 1 (lagret verdi)
16 println(once()) // Femte kall, vil returnere og skrive ut 1 (lagret verdi)
17}
Koden ovenfor bruker sync.OnceValue til å øke c-variabelen med 1. I denne koden brukes sync.OnceValue til å opprette once-funksjonen, og selv om once-funksjonen kalles flere ganger, vil c-variabelen bare øke én gang og returnere 1.
OnceValues
OnceValues fungerer på samme måte som OnceValue, men kan returnere flere verdier.
1package main
2
3import "sync"
4
5func main() {
6 c := 0
7 once := sync.OnceValues(func() (int, int) { // Opprett en OnceValues
8 c += 1 // Øk c
9 return c, c // Returner c og c
10 })
11
12 a, b := once() // Første kall, vil returnere (1, 1) og skrive ut
13 println(a, b)
14 a, b = once() // Andre kall, vil returnere (1, 1) (lagrede verdier) og skrive ut
15 println(a, b)
16 a, b = once() // Tredje kall, vil returnere (1, 1) (lagrede verdier) og skrive ut
17 println(a, b)
18 a, b = once() // Fjerde kall, vil returnere (1, 1) (lagrede verdier) og skrive ut
19 println(a, b)
20 a, b = once() // Femte kall, vil returnere (1, 1) (lagrede verdier) og skrive ut
21 println(a, b)
22}
Koden ovenfor bruker sync.OnceValues til å øke c-variabelen med 1. I denne koden brukes sync.OnceValues til å opprette once-funksjonen, og selv om once-funksjonen kalles flere ganger, vil c-variabelen bare øke én gang og returnere 1.
Atomic
atomic-pakken tilbyr atomiske operasjoner. atomic-pakken tilbyr metoder som Add, CompareAndSwap, Load, Store og Swap, men nylig anbefales bruk av typer som Int64, Uint64 og Pointer.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{} // Opprett en WaitGroup
10 c := atomic.Int64{} // En atomisk int64 for trådsikker telling
11
12 for i := 0; i < 100 ; i++ { // Start 100 goroutiner
13 wg.Add(1) // Legg til 1 til WaitGroup-telleren
14 go func() {
15 defer wg.Done() // Signaliser at goroutinen er ferdig når den avsluttes
16 c.Add(1) // Øk teller atomisk
17 }()
18 }
19
20 wg.Wait() // Vent til alle goroutiner er ferdige
21 println(c.Load()) // Skriv ut den endelige verdien av c
22}
Dette er et eksempel som ble brukt tidligere. Koden øker c-variabelen atomisk ved hjelp av atomic.Int64-typen. Med Add-metoden og Load-metoden kan variabler økes atomisk og leses. I tillegg kan verdier lagres med Store-metoden, verdier byttes med Swap-metoden, og verdier sammenlignes og byttes hvis de er passende med CompareAndSwap-metoden.
Cond
sync.Cond
cond-pakken tilbyr betingelsesvariabler. cond-pakken kan opprettes med sync.Cond og tilbyr metodene Wait, Signal og Broadcast.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 c := sync.NewCond(&sync.Mutex{}) // Opprett en Cond med en Mutex
9 ready := false // En flaggvariabel
10
11 go func() {
12 c.L.Lock() // Lås Mutexen assosiert med Cond
13 ready = true // Sett flagget til true
14 c.Signal() // Signaliser at betingelsen er oppfylt
15 c.L.Unlock() // Lås opp Mutexen
16 }()
17
18 c.L.Lock() // Lås Mutexen
19 for !ready { // Vent til ready er true
20 c.Wait() // Frigi låsen, vent på signal, og ta låsen igjen
21 }
22 c.L.Unlock() // Lås opp Mutexen
23
24 println("Ready!") // Skriv ut "Ready!"
25}
Koden ovenfor bruker sync.Cond til å vente til ready-variabelen blir true. I denne koden brukes sync.Cond til å vente til ready-variabelen blir true, og deretter skrives Ready! ut. Ved å bruke sync.Cond på denne måten kan flere goroutiner vente til en bestemt betingelse er oppfylt.
Ved å bruke dette kan en enkel queue implementeres.
1package queue
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Node[T any] struct {
9 Value T
10 Next *Node[T]
11}
12
13type Queue[T any] struct {
14 sync.Mutex // Mutex for å beskytte køen
15 Cond *sync.Cond // Betingelsesvariabel for å signalisere/vente
16 Head *Node[T] // Hode av køen
17 Tail *Node[T] // Hale av køen
18 Len int // Antall elementer i køen
19}
20
21func New[T any]() *Queue[T] {
22 q := &Queue[T]{}
23 q.Cond = sync.NewCond(&q.Mutex) // Initialiser Cond med køens Mutex
24 return q
25}
26
27func (q *Queue[T]) Push(value T) {
28 q.Lock() // Lås køen
29 defer q.Unlock() // Lås opp køen når funksjonen avsluttes
30
31 node := &Node[T]{Value: value} // Opprett en ny node
32 if q.Len == 0 {
33 q.Head = node // Hvis køen er tom, er den nye noden både hode og hale
34 q.Tail = node
35 } else {
36 q.Tail.Next = node // Ellers, legg til noden på slutten
37 q.Tail = node
38 }
39 q.Len++ // Øk kølengden
40 q.Cond.Signal() // Signaliser at det er et nytt element
41}
42
43func (q *Queue[T]) Pop() T {
44 q.Lock() // Lås køen
45 defer q.Unlock() // Lås opp køen når funksjonen avsluttes
46
47 for q.Len == 0 { // Vent til køen ikke er tom
48 q.Cond.Wait() // Frigi låsen, vent på signal, og ta låsen igjen
49 }
50
51 node := q.Head // Få hodet av køen
52 q.Head = q.Head.Next // Flytt hodet til neste node
53 q.Len-- // Reduser kølengden
54 return node.Value // Returner verdien av den fjernede noden
55}
Ved å bruke sync.Cond på denne måten kan du effektivt vente og gjenoppta operasjoner når en betingelse er oppfylt, i stedet for å bruke en spin-lock som bruker mye CPU.
Semaphore
golang.org/x/sync/semaphore.Semaphore
semaphore-pakken tilbyr semaforer. semaphore-pakken kan opprettes med golang.org/x/sync/semaphore.Semaphore og tilbyr metodene Acquire, Release og TryAcquire.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/semaphore"
7)
8
9func main() {
10 s := semaphore.NewWeighted(1) // Opprett en vektet semafor med en vekt på 1
11
12 if s.TryAcquire(1) { // Forsøk å skaffe 1 vekt fra semaforen
13 fmt.Println("Acquired!") // Skriv ut "Acquired!" hvis det lykkes
14 } else {
15 fmt.Println("Not Acquired!") // Skriv ut "Not Acquired!" hvis det mislykkes
16 }
17
18 s.Release(1) // Frigi 1 vekt fra semaforen
19}
Koden ovenfor oppretter en semafor ved hjelp av semaphore og viser hvordan man skaffer og frigjør semaforen ved hjelp av Acquire- og Release-metodene. I denne koden har vi vist hvordan man skaffer og frigjør en semafor ved hjelp av semaphore.
Avslutning
Det grunnleggende bør være tilstrekkelig for nå. Basert på innholdet i denne artikkelen håper jeg at dere har forstått hvordan man håndterer samtidighet ved hjelp av goroutiner, og at dere kan bruke det i praksis. Jeg håper denne artikkelen har vært nyttig for dere. Takk.