Go Concurrency Starter Pack
Översikt
Kort introduktion
Go-språket erbjuder många verktyg för hantering av samtidighet. I den här artikeln kommer vi att introducera några av dem och tillhörande tekniker.
Gorutin?
En goroutine är en ny form av samtidighet-modell som stöds i Go-språket. Normalt sett mottar ett program OS-trådar från operativsystemet för att utföra flera uppgifter samtidigt, och utför arbete parallellt motsvarande antalet kärnor. För att utföra samtidighet i mindre enheter skapar man green threads i användarutrymmet, vilket gör att flera green threads kan köras inom en enda OS-tråd. Goroutines har dock gjort denna form av green threads ännu mindre och effektivare. Dessa goroutines använder mindre minne än trådar och kan skapas och bytas ut snabbare än trådar.
För att använda en goroutine behöver man bara använda nyckelordet go. Detta gör det möjligt att intuitivt köra synkron kod asynkront under programutvecklingen.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch := make(chan struct{})
10 go func() {
11 defer close(ch)
12 time.Sleep(1 * time.Second)
13 fmt.Println("Hello, World!")
14 }()
15
16 fmt.Println("Waiting for goroutine...")
17 for range ch {}
18}
Denna kod omvandlar enkelt synkron kod som vilar i 1 sekund och sedan skriver ut Hello, World! till ett asynkront flöde. Även om detta exempel är enkelt, kommer läsbarheten, synligheten och förståelsen av koden att förbättras ytterligare jämfört med befintliga metoder som async await eller promise, om man omvandlar lite mer komplex kod från synkron till asynkron.
I många fall kan dock dålig goroutine-kod skapas om man inte förstår detta flöde av att helt enkelt anropa synkron kod asynkront och flöden som fork & join (liknande ett divide and conquer-flöde). I denna artikel kommer vi att introducera några metoder och tekniker för att förbereda sig för sådana fall.
Samtidighetshantering
context
Att context dyker upp som den första hanteringstekniken kan vara överraskande. Men i Go-språket går context bortom en enkel avbrytningsfunktion och spelar en utmärkt roll i att hantera hela arbetsträdet. För dem som inte känner till det, kommer jag kortfattat att förklara paketet.
1package main
2
3func main() {
4 // Skapar en context med avbrytningsfunktion.
5 ctx, cancel := context.WithCancel(context.Background())
6 // Säkerställer att avbrytningsfunktionen anropas när funktionen returnerar.
7 defer cancel()
8
9 // Startar en goroutine.
10 go func() {
11 // Väntar tills contexten är klar.
12 <-ctx.Done()
13 // Skriver ut ett meddelande när contexten är klar.
14 fmt.Println("Context is done!")
15 }()
16
17 // Vilar i 1 sekund.
18 time.Sleep(1 * time.Second)
19
20 // Avbryter contexten.
21 cancel()
22
23 // Vilar i 1 sekund.
24 time.Sleep(1 * time.Second)
25}
Koden ovan använder context för att skriva ut Context is done! efter 1 sekund. context kan kontrollera avbrytningsstatus via metoden Done(), och erbjuder olika avbrytningsmetoder via metoder som WithCancel, WithTimeout, WithDeadline, WithValue.
Låt oss skapa ett enkelt exempel. Anta att ni skriver kod som använder aggregator-mönstret för att hämta user, post och comment för att hämta viss data. Och om alla förfrågningar måste slutföras inom 2 sekunder, kan ni skriva det enligt följande:
1package main
2
3func main() {
4 // Skapar en context med en tidsgräns på 2 sekunder.
5 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
6 // Säkerställer att avbrytningsfunktionen anropas när funktionen returnerar.
7 defer cancel()
8
9 // Skapar en kanal för att signalera att all data har hämtats.
10 ch := make(chan struct{})
11 // Startar en goroutine för att hämta data.
12 go func() {
13 // Säkerställer att kanalen stängs när goroutinen returnerar.
14 defer close(ch)
15 // Hämtar användare, inlägg och kommentarer med hjälp av context.
16 user := getUser(ctx)
17 post := getPost(ctx)
18 comment := getComment(ctx)
19
20 // Skriver ut den hämtade datan.
21 fmt.Println(user, post, comment)
22 }()
23
24 // Använder select för att hantera tidsgräns eller att all data har hämtats.
25 select {
26 case <-ctx.Done():
27 // Skriver ut "Timeout!" om tidsgränsen överskrids.
28 fmt.Println("Timeout!")
29 case <-ch:
30 // Skriver ut "All data is fetched!" om all data har hämtats.
31 fmt.Println("All data is fetched!")
32 }
33}
Koden ovan skriver ut Timeout! om all data inte hämtas inom 2 sekunder, och All data is fetched! om all data hämtas. På detta sätt kan context användas för att enkelt hantera avbrytning och tidsgränser även i kod där flera goroutines körs.
Relaterade funktioner och metoder för context finns tillgängliga på godoc context. Vi hoppas att ni kan lära er de enkla och använda dem bekvämt.
channel
unbuffered channel
En channel är ett verktyg för kommunikation mellan goroutines. En channel kan skapas med make(chan T). Här är T datatypen som channel kommer att överföra. Data kan skickas och tas emot via <- i en channel, och channel kan stängas med close.
1package main
2
3import "fmt"
4
5func main() {
6 // Skapar en obuffrad kanal av typen int.
7 ch := make(chan int)
8 // Startar en goroutine.
9 go func() {
10 // Skickar 1 till kanalen.
11 ch <- 1
12 // Skickar 2 till kanalen.
13 ch <- 2
14 // Stänger kanalen.
15 close(ch)
16 }()
17
18 // Itererar över kanalen och skriver ut mottagna värden.
19 for i := range ch {
20 fmt.Println(i)
21 }
22}
Koden ovan skriver ut 1 och 2 med hjälp av en channel. Denna kod visar endast att värden skickas och tas emot via en channel. Men en channel erbjuder fler funktioner än så. Låt oss först titta på buffered channel och unbuffered channel. Innan vi börjar, är exemplet ovan en unbuffered channel, vilket innebär att sändning och mottagning av data i kanalen måste ske samtidigt. Om detta inte sker samtidigt kan en deadlock uppstå.
buffered channel
Vad händer om koden ovan inte bara är en enkel utskrift, utan två processer som utför tunga operationer? Om den andra processen fastnar under en längre tid medan den läser och bearbetar, kommer även den första processen att stanna under samma tid. Vi kan använda en buffered channel för att förhindra denna situation.
1package main
2
3import "fmt"
4
5func main() {
6 // Skapar en buffrad kanal av typen int med en buffertstorlek på 2.
7 ch := make(chan int, 2)
8 // Startar en goroutine.
9 go func() {
10 // Skickar 1 till kanalen.
11 ch <- 1
12 // Skickar 2 till kanalen.
13 ch <- 2
14 // Stänger kanalen.
15 close(ch)
16 }()
17
18 // Itererar över kanalen och skriver ut mottagna värden.
19 for i := range ch {
20 fmt.Println(i)
21 }
22}
Koden ovan skriver ut 1 och 2 med hjälp av en buffered channel. Denna kod använder en buffered channel för att möjliggöra att sändning och mottagning av data i kanalen inte behöver ske samtidigt. Genom att lägga till en buffert till kanalen skapas ett utrymme motsvarande buffertens längd, vilket kan förhindra fördröjningar i arbetet som orsakas av efterföljande uppgifter.
select
När man hanterar flera kanaler kan man enkelt implementera en fan-in-struktur med hjälp av select-satsen.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 // Skapar tre buffrade kanaler av typen int med en buffertstorlek på 10.
10 ch1 := make(chan int, 10)
11 ch2 := make(chan int, 10)
12 ch3 := make(chan int, 10)
13
14 // Startar en goroutine som skickar 1 till ch1 varje sekund.
15 go func() {
16 for {
17 ch1 <- 1
18 time.Sleep(1 * time.Second)
19 }
20 }()
21 // Startar en goroutine som skickar 2 till ch2 varannan sekund.
22 go func() {
23 for {
24 ch2 <- 2
25 time.Sleep(2 * time.Second)
26 }
27 }()
28 // Startar en goroutine som skickar 3 till ch3 var tredje sekund.
29 go func() {
30 for {
31 ch3 <- 3
32 time.Sleep(3 * time.Second)
33 }
34 }()
35
36 // Itererar 3 gånger för att ta emot värden från kanalerna.
37 for i := 0; i < 3; i++ {
38 // Använder select för att ta emot från den första kanalen som är redo.
39 select {
40 case v := <-ch1:
41 fmt.Println(v)
42 case v := <-ch2:
43 fmt.Println(v)
44 case v := <-ch3:
45 fmt.Println(v)
46 }
47 }
48}
Koden ovan skapar tre kanaler som periodiskt skickar 1, 2 och 3, och använder select för att ta emot och skriva ut värden från kanalerna. På detta sätt kan select användas för att ta emot data från flera kanaler samtidigt och bearbeta värdena så snart de tas emot från en kanal.
for range
En channel kan enkelt ta emot data med hjälp av for range. När for range används med en kanal kommer den att köras varje gång data läggs till i kanalen, och slutar loopen när kanalen stängs.
1package main
2
3import "fmt"
4
5func main() {
6 // Skapar en obuffrad kanal av typen int.
7 ch := make(chan int)
8 // Startar en goroutine.
9 go func() {
10 // Skickar 1 till kanalen.
11 ch <- 1
12 // Skickar 2 till kanalen.
13 ch <- 2
14 // Stänger kanalen.
15 close(ch)
16 }()
17
18 // Itererar över kanalen och skriver ut mottagna värden.
19 for i := range ch {
20 fmt.Println(i)
21 }
22}
Koden ovan skriver ut 1 och 2 med hjälp av en channel. Denna kod använder for range för att ta emot och skriva ut data varje gång data läggs till i kanalen. Och loopen avslutas när kanalen stängs.
Som nämnts flera gånger ovan, kan denna syntax också användas för enkel synkronisering.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 // Skapar en obuffrad kanal av typen struct{}.
10 ch := make(chan struct{})
11 // Startar en goroutine.
12 go func() {
13 // Säkerställer att kanalen stängs när goroutinen returnerar.
14 defer close(ch)
15 // Vilar i 1 sekund.
16 time.Sleep(1 * time.Second)
17 // Skriver ut "Hello, World!".
18 fmt.Println("Hello, World!")
19 }()
20
21 // Skriver ut "Waiting for goroutine...".
22 fmt.Println("Waiting for goroutine...")
23 // Väntar tills kanalen stängs.
24 for range ch {}
25}
Koden ovan vilar i 1 sekund och skriver sedan ut Hello, World!. Denna kod ändrar synkron kod till asynkron kod med hjälp av en channel. På detta sätt kan channel användas för att enkelt ändra synkron kod till asynkron kod och ställa in join-punkter.
etc
- Att skicka eller ta emot data till en nil channel kan leda till en oändlig loop och därmed en deadlock.
- Att skicka data efter att en channel har stängts kommer att orsaka en panic.
- Även om en channel inte explicit stängs, kommer GC att stänga den vid insamling.
mutex
spinlock
spinlock är en synkroniseringsmetod som kontinuerligt försöker låsa genom att köra en loop. I Go-språket kan man enkelt implementera en spinlock med hjälp av pekare.
1package spinlock
2
3import (
4 "runtime"
5 "sync/atomic"
6)
7
8// SpinLock representerar en spinlock.
9type SpinLock struct {
10 lock uintptr // 0 för olåst, 1 för låst
11}
12
13// Lock försöker låsa spinlåset.
14func (s *SpinLock) Lock() {
15 // Fortsätter att försöka låsa tills det lyckas.
16 for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) {
17 // Ger bort CPU-tid för att undvika att blockera.
18 runtime.Gosched()
19 }
20}
21
22// Unlock släpper spinlåset.
23func (s *SpinLock) Unlock() {
24 // Ställer in låset till olåst (0).
25 atomic.StoreUintptr(&s.lock, 0)
26}
27
28// NewSpinLock skapar och returnerar en ny SpinLock.
29func NewSpinLock() *SpinLock {
30 return &SpinLock{}
31}
Koden ovan implementerar paketet spinlock. Denna kod implementerar SpinLock med hjälp av paketet sync/atomic. Metoden Lock försöker låsa med atomic.CompareAndSwapUintptr, och metoden Unlock släpper låset med atomic.StoreUintptr. Eftersom denna metod kontinuerligt försöker låsa utan paus, kommer den att fortsätta använda CPU:n tills låset erhålls, vilket kan leda till en oändlig loop. Därför är det bäst att använda spinlock för enkel synkronisering eller för korta tidsperioder.
sync.Mutex
En mutex är ett verktyg för synkronisering mellan goroutines. mutex som tillhandahålls av paketet sync erbjuder metoder som Lock, Unlock, RLock, RUnlock. En mutex kan skapas med sync.Mutex, och man kan också använda läs-/skrivlås med sync.RWMutex.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 // Deklarerar en Mutex.
9 var mu sync.Mutex
10 // Deklarerar en räknare.
11 var count int
12
13 // Startar en goroutine.
14 go func() {
15 // Låser Mutexen.
16 mu.Lock()
17 // Ökar räknaren.
18 count++
19 // Låser upp Mutexen.
20 mu.Unlock()
21 }()
22
23 // Låser Mutexen.
24 mu.Lock()
25 // Ökar räknaren.
26 count++
27 // Låser upp Mutexen.
28 mu.Unlock()
29
30 // Skriver ut räknaren.
31 println(count)
32}
I koden ovan kommer två goroutines nästan samtidigt att komma åt samma count-variabel. Genom att använda en mutex för att göra koden som kommer åt count-variabeln till ett kritiskt avsnitt, kan samtidig åtkomst till count-variabeln förhindras. Då kommer denna kod att skriva ut 2 oavsett hur många gånger den körs.
sync.RWMutex
sync.RWMutex är en mutex som kan användas med separata läs- och skrivlås. Metoderna RLock och RUnlock kan användas för att låsa och släppa läslåset.
1package cmap
2
3import (
4 "sync"
5)
6
7// ConcurrentMap är en trådsäker mapp.
8type ConcurrentMap[K comparable, V any] struct {
9 sync.RWMutex // Inbäddad RWMutex för läs-/skrivlås.
10 data map[K]V // Den underliggande kartan.
11}
12
13// Get hämtar ett värde från kartan.
14func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
15 m.RLock() // Låser för läsning.
16 defer m.RUnlock() // Säkerställer att läslåset släpps.
17
18 value, ok := m.data[key] // Hämtar värdet.
19 return value, ok
20}
21
22// Set ställer in ett värde i kartan.
23func (m *ConcurrentMap[K, V]) Set(key K, value V) {
24 m.Lock() // Låser för skrivning.
25 defer m.Unlock() // Säkerställer att skrivlåset släpps.
26
27 m.data[key] = value // Ställer in värdet.
28}
Koden ovan implementerar ConcurrentMap med hjälp av sync.RWMutex. I denna kod låser Get-metoden för läsning och Set-metoden låser för skrivning, vilket möjliggör säker åtkomst och modifiering av data-kartan. Anledningen till att läslås behövs är att i fall med många enkla läsoperationer, kan flera goroutines utföra läsoperationer samtidigt genom att bara låsa för läsning, utan att låsa för skrivning. Detta kan förbättra prestandan genom att endast låsa för läsning i fall där ingen statusändring kräver ett skrivlås.
fakelock
fakelock är ett enkelt knep som implementerar sync.Locker. Denna struktur tillhandahåller samma metoder som sync.Mutex, men utför ingen faktisk operation.
1package fakelock
2
3// FakeLock är en dummy-implementation av sync.Locker.
4type FakeLock struct{}
5
6// Lock gör ingenting.
7func (f *FakeLock) Lock() {}
8
9// Unlock gör ingenting.
10func (f *FakeLock) Unlock() {}
Koden ovan implementerar paketet fakelock. Detta paket implementerar sync.Locker och tillhandahåller metoderna Lock och Unlock, men utför ingen faktisk operation. Jag kommer att beskriva varför sådan kod behövs om tillfälle ges.
waitgroup
sync.WaitGroup
sync.WaitGroup är ett verktyg för att vänta tills alla goroutines är klara med sitt arbete. Det tillhandahåller metoderna Add, Done, Wait. Metoden Add lägger till antalet goroutines, och metoden Done signalerar att en goroutine har slutfört sitt arbete. Metoden Wait väntar tills alla goroutines har slutfört sitt arbete.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 // Skapar en WaitGroup.
10 wg := sync.WaitGroup{}
11 // Skapar en atomisk Int64 för att räkna.
12 c := atomic.Int64{}
13
14 // Startar 100 goroutines.
15 for i := 0; i < 100 ; i++ {
16 // Ökar räknaren i WaitGroup.
17 wg.Add(1)
18 // Startar en goroutine.
19 go func() {
20 // Säkerställer att Done anropas när goroutinen avslutas.
21 defer wg.Done()
22 // Ökar den atomiska räknaren.
23 c.Add(1)
24 }()
25 }
26
27 // Väntar tills alla goroutines är klara.
28 wg.Wait()
29 // Skriver ut det slutliga värdet av räknaren.
30 println(c.Load())
31}
Koden ovan använder sync.WaitGroup för att 100 goroutines samtidigt ska lägga till ett värde till variabeln c. I denna kod används sync.WaitGroup för att vänta tills alla goroutines är klara, och sedan skrivs det summerade värdet i variabeln c ut. Även om enbart kanaler räcker för att fork & join några få uppgifter, är sync.WaitGroup ett bra alternativ när man fork & join ett stort antal uppgifter.
with slice
Om det används tillsammans med slices, kan waitgroup vara ett utmärkt verktyg för att hantera parallella exekveringsuppgifter utan lås.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "math/rand" // Korrigerat från "rand" till "math/rand"
7)
8
9func main() {
10 var wg sync.WaitGroup
11 arr := [10]int{} // Deklarerar en array med 10 heltal.
12
13 for i := 0; i < 10; i++ {
14 wg.Add(1) // Lägger till 1 till WaitGroup-räknaren.
15 go func(id int) {
16 defer wg.Done() // Minskar WaitGroup-räknaren när goroutinen avslutas.
17
18 arr[id] = rand.Intn(100) // Genererar ett slumpmässigt heltal och tilldelar det till arrayen.
19 }(i) // Skickar loopvariabeln 'i' som ett argument till goroutinen.
20 }
21
22 wg.Wait() // Väntar tills alla goroutines har slutförts.
23 fmt.Println("Done")
24
25 for i, v := range arr {
26 fmt.Printf("arr[%d] = %d\n", i, v) // Skriver ut varje element i arrayen.
27 }
28}
Koden ovan använder enbart waitgroup för att varje goroutine samtidigt ska generera 10 slumpmässiga heltal och lagra dem på den tilldelade indexpositionen. I denna kod används waitgroup för att vänta tills alla goroutines är klara, och sedan skrivs Done ut. På detta sätt kan waitgroup användas för att flera goroutines ska utföra arbete samtidigt, lagra data utan lås tills alla goroutines är klara, och sedan utföra efterbehandling i en batch.
golang.org/x/sync/errgroup.ErrGroup
errgroup är ett paket som utökar sync.WaitGroup. Till skillnad från sync.WaitGroup avbryter errgroup alla goroutines och returnerar ett fel om ett fel uppstår i någon av goroutines-operationerna.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/errgroup"
7)
8
9func main() {
10 // Skapar en errgroup och en context från bakgrundscontexten.
11 g, ctx := errgroup.WithContext(context.Background())
12 // Kontexten används inte direkt i detta exempel, men kan användas för avbrytning.
13 _ = ctx
14
15 // Startar 10 goroutines.
16 for i := 0; i < 10; i++ {
17 // Skapar en lokal kopia av i för att undvika att den ändras under loopen.
18 i := i
19 // Lägger till en goroutine till errgroup.
20 g.Go(func() error {
21 // Simulerar ett fel om i är 5.
22 if i == 5 {
23 return fmt.Errorf("error")
24 }
25 // Returnerar nil om inget fel uppstår.
26 return nil
27 })
28 }
29
30 // Väntar tills alla goroutines är klara och kontrollerar om något fel uppstod.
31 if err := g.Wait(); err != nil {
32 // Skriver ut felet om ett sådant uppstod.
33 fmt.Println(err)
34 }
35}
Koden ovan använder errgroup för att skapa 10 goroutines och orsakar ett fel i den femte goroutinen. Jag orsakade avsiktligt ett fel i den femte goroutinen för att visa fallet där ett fel uppstår. Men i praktiken kan errgroup användas för att skapa goroutines, och sedan hantera olika efterbearbetningar för de fall där fel uppstår i varje goroutine.
once
Ett verktyg för att köra kod som bara ska köras en gång. Relaterad kod kan köras via nedanstående konstruktorer.
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 säkerställer att den angivna funktionen endast körs en enda gång under hela programmets exekvering.
1package main
2
3import "sync"
4
5func main() {
6 // Skapar en funktion 'once' som endast körs en gång.
7 once := sync.OnceFunc(func() {
8 // Denna rad kommer endast att skrivas ut en gång.
9 println("Hello, World!")
10 })
11
12 // Anropar 'once' flera gånger.
13 once()
14 once()
15 once()
16 once()
17 once()
18}
Koden ovan använder sync.OnceFunc för att skriva ut Hello, World!. I denna kod skapas once-funktionen med sync.OnceFunc, och även om once-funktionen anropas flera gånger, skrivs Hello, World! endast ut en gång.
OnceValue
OnceValue säkerställer inte bara att den angivna funktionen körs exakt en gång, utan den lagrar också funktionens returvärde och returnerar det lagrade värdet vid efterföljande anrop.
1package main
2
3import "sync"
4
5func main() {
6 // Deklarerar en räknare.
7 c := 0
8 // Skapar en funktion 'once' som endast körs en gång och lagrar dess returvärde.
9 once := sync.OnceValue(func() int {
10 // Ökar räknaren och returnerar dess nya värde.
11 c += 1
12 return c
13 })
14
15 // Anropar 'once' flera gånger och skriver ut returvärdet.
16 println(once())
17 println(once())
18 println(once())
19 println(once())
20 println(once())
21}
Koden ovan använder sync.OnceValue för att öka variabeln c med 1. I denna kod skapas once-funktionen med sync.OnceValue, och även om once-funktionen anropas flera gånger, returnerar den värdet 1, vilket är det värde som c ökade till en gång.
OnceValues
OnceValues fungerar på samma sätt som OnceValue, men kan returnera flera värden.
1package main
2
3import "sync"
4
5func main() {
6 // Deklarerar en räknare.
7 c := 0
8 // Skapar en funktion 'once' som endast körs en gång och lagrar dess returvärden.
9 once := sync.OnceValues(func() (int, int) {
10 // Ökar räknaren och returnerar dess nya värde två gånger.
11 c += 1
12 return c, c
13 })
14
15 // Anropar 'once' flera gånger och skriver ut returvärdena.
16 a, b := once()
17 println(a, b)
18 a, b = once()
19 println(a, b)
20 a, b = once()
21 println(a, b)
22 a, b = once()
23 println(a, b)
24 a, b = once()
25 println(a, b)
26}
Koden ovan använder sync.OnceValues för att öka variabeln c med 1. I denna kod skapas once-funktionen med sync.OnceValues, och även om once-funktionen anropas flera gånger, returnerar den värdet 1, vilket är det värde som c ökade till en gång.
atomic
Paketet atomic tillhandahåller atomära operationer. Paketet atomic tillhandahåller metoder som Add, CompareAndSwap, Load, Store, Swap, men nyligen rekommenderas användning av typer som Int64, Uint64, Pointer.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 // Skapar en WaitGroup.
10 wg := sync.WaitGroup{}
11 // Skapar en atomisk Int64 för att räkna.
12 c := atomic.Int64{}
13
14 // Startar 100 goroutines.
15 for i := 0; i < 100 ; i++ {
16 // Ökar räknaren i WaitGroup.
17 wg.Add(1)
18 // Startar en goroutine.
19 go func() {
20 // Säkerställer att Done anropas när goroutinen avslutas.
21 defer wg.Done()
22 // Ökar den atomiska räknaren.
23 c.Add(1)
24 }()
25 }
26
27 // Väntar tills alla goroutines är klara.
28 wg.Wait()
29 // Skriver ut det slutliga värdet av räknaren.
30 println(c.Load())
31}
Detta var ett tidigare använt exempel. Det är kod som atomärt ökar variabeln c med typen atomic.Int64. Med metoderna Add och Load kan variabler ökas atomärt och läsas. Dessutom kan värden lagras med metoden Store, värden bytas ut med metoden Swap, och värden jämföras och bytas ut om de är lämpliga med metoden CompareAndSwap.
cond
sync.Cond
Paketet cond tillhandahåller villkorsvariabler. Paketet cond kan skapas med sync.Cond och tillhandahåller metoderna Wait, Signal, Broadcast.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 // Skapar en ny Cond med en Mutex.
9 c := sync.NewCond(&sync.Mutex{})
10 // Flagg för att indikera om det är klart.
11 ready := false
12
13 // Startar en goroutine.
14 go func() {
15 // Låser Mutexen associerad med Cond.
16 c.L.Lock()
17 // Sätter flaggan till true.
18 ready = true
19 // Signalerar en väntande goroutine.
20 c.Signal()
21 // Låser upp Mutexen.
22 c.L.Unlock()
23 }()
24
25 // Låser Mutexen associerad med Cond.
26 c.L.Lock()
27 // Väntar tills 'ready' blir true.
28 for !ready {
29 c.Wait()
30 }
31 // Låser upp Mutexen.
32 c.L.Unlock()
33
34 // Skriver ut "Ready!".
35 println("Ready!")
36}
Koden ovan använder sync.Cond för att vänta tills variabeln ready blir true. I denna kod används sync.Cond för att vänta tills variabeln ready blir true, och sedan skrivs Ready! ut. På detta sätt kan sync.Cond användas för att flera goroutines samtidigt ska vänta tills ett specifikt villkor är uppfyllt.
Med detta som grund kan en enkel queue implementeras.
1package queue
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8// Node representerar en nod i kön.
9type Node[T any] struct {
10 Value T // Värdet som lagras i noden.
11 Next *Node[T] // Pekare till nästa nod.
12}
13
14// Queue representerar en generisk kö.
15type Queue[T any] struct {
16 sync.Mutex // Mutex för att skydda kön.
17 Cond *sync.Cond // Villkorsvariabel för att signalera när kön är tom/full.
18 Head *Node[T] // Pekare till könens huvud.
19 Tail *Node[T] // Pekare till könens svans.
20 Len int // Antal element i kön.
21}
22
23// New skapar och returnerar en ny tom kö.
24func New[T any]() *Queue[T] {
25 q := &Queue[T]{}
26 q.Cond = sync.NewCond(&q.Mutex) // Initierar villkorsvariabeln med könens mutex.
27 return q
28}
29
30// Push lägger till ett element i kön.
31func (q *Queue[T]) Push(value T) {
32 q.Lock() // Låser könen.
33 defer q.Unlock() // Låser upp könen när funktionen returnerar.
34
35 node := &Node[T]{Value: value} // Skapar en ny nod.
36 if q.Len == 0 {
37 q.Head = node // Om könen är tom, blir den nya noden både huvud och svans.
38 q.Tail = node
39 } else {
40 q.Tail.Next = node // Annars läggs den nya noden till efter svansen.
41 q.Tail = node
42 }
43 q.Len++ // Ökar könens längd.
44 q.Cond.Signal() // Signalerar en väntande goroutine att ett element har lagts till.
45}
46
47// Pop tar bort och returnerar det första elementet från kön.
48func (q *Queue[T]) Pop() T {
49 q.Lock() // Låser könen.
50 defer q.Unlock() // Låser upp könen när funktionen returnerar.
51
52 for q.Len == 0 {
53 q.Cond.Wait() // Väntar tills könen inte är tom.
54 }
55
56 node := q.Head // Hämtar huvudet.
57 q.Head = q.Head.Next // Flyttar huvudet till nästa nod.
58 q.Len-- // Minskar könens längd.
59 return node.Value // Returnerar värdet från den borttagna noden.
60}
På detta sätt, genom att använda sync.Cond, kan man effektivt vänta och återuppta operationer när villkoret är uppfyllt, istället för att använda en spin-lock som förbrukar mycket CPU-användning.
semaphore
golang.org/x/sync/semaphore.Semaphore
Paketet semaphore tillhandahåller semaforer. Paketet semaphore kan skapas med golang.org/x/sync/semaphore.Semaphore och tillhandahåller metoderna Acquire, Release, TryAcquire.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/semaphore"
7)
8
9func main() {
10 // Skapar en viktad semafor med en vikt på 1.
11 s := semaphore.NewWeighted(1)
12
13 // Försöker att förvärva en vikt på 1 från semaforen.
14 if s.TryAcquire(1) {
15 // Skriver ut "Acquired!" om förvärvet lyckades.
16 fmt.Println("Acquired!")
17 } else {
18 // Skriver ut "Not Acquired!" om förvärvet misslyckades.
19 fmt.Println("Not Acquired!")
20 }
21
22 // Släpper en vikt på 1 från semaforen.
23 s.Release(1)
24}
Koden ovan använder semaphore för att skapa en semafor, och sedan använder semaforen för att förvärva semaforen med metoden Acquire och släppa semaforen med metoden Release. I denna kod visade jag hur man förvärvar och släpper en semafor med hjälp av semaphore.
Avslutning
Grundläggande innehåll bör räcka med detta. Med utgångspunkt i denna artikels innehåll hoppas jag att ni förstår hur man hanterar samtidighet med goroutines och kan använda dem i praktiken. Jag hoppas att denna artikel har varit till hjälp för er. Tack.