Go-Gleichzeitigkeits-Starterpaket
Überblick
Kurze Einführung
Die Go-Sprache bietet zahlreiche Werkzeuge für die Verwaltung von Nebenläufigkeit. In diesem Artikel werden wir einige davon und einige Tricks vorstellen.
Goroutinen?
Eine Goroutine ist ein neues Modell der Nebenläufigkeit, das von der Go-Sprache unterstützt wird. Im Allgemeinen erhält ein Programm OS-Threads vom Betriebssystem, um mehrere Aufgaben gleichzeitig auszuführen, und führt Aufgaben parallel entsprechend der Anzahl der Kerne aus. Und um eine Nebenläufigkeit in kleineren Einheiten auszuführen, werden im Userland Green Threads erstellt, so dass mehrere Green Threads innerhalb eines OS-Threads rotierend Aufgaben ausführen. Goroutinen haben jedoch diese Form von Green Threads kleiner und effizienter gemacht. Solche Goroutinen verwenden weniger Speicher als Threads und können schneller erstellt und ausgetauscht werden als Threads.
Um eine Goroutine zu verwenden, genügt es, das Schlüsselwort go
zu verwenden. Dies ermöglicht es, im Zuge der Programmierung synchronen Code intuitiv als asynchronen Code auszuführen.
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}
Dieser Code ändert einfach synchronen Code, der eine Sekunde wartet und dann Hello, World!
ausgibt, in einen asynchronen Ablauf. Das gegenwärtige Beispiel ist einfach, aber wenn etwas komplexerer Code von synchron zu asynchron geändert wird, werden die Lesbarkeit, Sichtbarkeit und das Verständnis des Codes besser als bei herkömmlichen Methoden wie async await oder Promise.
In vielen Fällen wird jedoch schlechter Goroutinen-Code erzeugt, wenn der Ablauf, in dem dieser synchrone Code einfach asynchron aufgerufen wird, und Abläufe wie fork & join
(einem Divide-and-Conquer-Ablauf ähnlich) nicht verstanden werden. In diesem Artikel werden einige Methoden und Techniken vorgestellt, um auf diese Fälle vorbereitet zu sein.
Verwaltung von Nebenläufigkeit
context
Die Tatsache, dass context
als erste Verwaltungstechnik auftaucht, mag überraschen. Aber in der Go-Sprache spielt context
eine herausragende Rolle bei der Verwaltung des gesamten Aufgabenbaums und geht über die reine Abbruchfunktion hinaus. Für diejenigen, die es nicht wissen, werden wir das entsprechende Paket kurz erläutern.
1package main
2
3func main() {
4 ctx, cancel := context.WithCancel(context.Background())
5 defer cancel()
6
7 go func() {
8 <-ctx.Done()
9 fmt.Println("Context is done!")
10 }()
11
12 time.Sleep(1 * time.Second)
13
14 cancel()
15
16 time.Sleep(1 * time.Second)
17}
Der obige Code ist ein Code, der Context is done!
nach einer Sekunde mit context
ausgibt. context
kann über die Done()
-Methode überprüfen, ob abgebrochen wurde, und bietet verschiedene Abbruchmethoden über Methoden wie WithCancel
, WithTimeout
, WithDeadline
und WithValue
.
Lassen Sie uns ein einfaches Beispiel erstellen. Nehmen wir an, Sie verwenden das Aggregator
-Muster, um Daten zu holen, und schreiben Code, um user
, post
und comment
zu holen. Und wenn alle Anfragen innerhalb von 2 Sekunden erfolgen müssen, können Sie sie wie folgt schreiben.
1package main
2
3func main() {
4 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
5 defer cancel()
6
7 ch := make(chan struct{})
8 go func() {
9 defer close(ch)
10 user := getUser(ctx)
11 post := getPost(ctx)
12 comment := getComment(ctx)
13
14 fmt.Println(user, post, comment)
15 }()
16
17 select {
18 case <-ctx.Done():
19 fmt.Println("Timeout!")
20 case <-ch:
21 fmt.Println("All data is fetched!")
22 }
23}
Der obige Code gibt Timeout!
aus, wenn nicht alle Daten innerhalb von 2 Sekunden abgerufen werden können, und gibt All data is fetched!
aus, wenn alle Daten abgerufen werden. Durch die Verwendung von context
auf diese Weise können Sie Abbruch und Timeout auch in Code einfach verwalten, der mehrere Goroutinen ausführt.
Eine Vielzahl von context
-bezogenen Funktionen und Methoden sind unter godoc context verfügbar. Ich hoffe, dass Sie durch das Erlernen der einfachen Dinge in der Lage sein werden, sie bequem zu nutzen.
channel
Ungepufferter Kanal
Ein channel
ist ein Werkzeug für die Kommunikation zwischen Goroutinen. Ein channel
kann mit make(chan T)
erstellt werden. Hier ist T
der Datentyp, den der channel
übertragen wird. Ein channel
kann mit <-
Daten senden und empfangen, und ein channel
kann mit close
geschlossen werden.
1package main
2
3func main() {
4 ch := make(chan int)
5 go func() {
6 ch <- 1
7 ch <- 2
8 close(ch)
9 }()
10
11 for i := range ch {
12 fmt.Println(i)
13 }
14}
Der obige Code ist ein Code, der channel
verwendet, um 1 und 2 auszugeben. Dieser Code zeigt nur das einfache Senden und Empfangen von Werten an channel
. channel
bietet jedoch mehr Funktionen als diese. Lassen Sie uns zunächst etwas über buffered channel
und unbuffered channel
lernen. Zunächst einmal ist das obige Beispiel ein unbuffered channel
, und das Senden von Daten an einen Kanal und das Empfangen von Daten müssen gleichzeitig erfolgen. Wenn diese Aktion nicht gleichzeitig erfolgt, kann es zu einem Deadlock kommen.
Gepufferter Kanal
Was ist, wenn der obige Code nicht nur eine einfache Ausgabe ist, sondern zwei Prozesse, die eine schwere Aufgabe ausführen? Wenn der zweite Prozess beim Lesen und Verarbeiten hängen bleibt, wird auch der erste Prozess für diese Zeit angehalten. Wir können buffered channel
verwenden, um dies zu verhindern.
1package main
2
3func main() {
4 ch := make(chan int, 2)
5 go func() {
6 ch <- 1
7 ch <- 2
8 close(ch)
9 }()
10
11 for i := range ch {
12 fmt.Println(i)
13 }
14}
Der obige Code ist ein Code, der buffered channel
verwendet, um 1 und 2 auszugeben. Dieser Code verwendet buffered channel
, um das Senden von Daten an den channel
und das Empfangen von Daten gleichzeitig zu ermöglichen. Durch das Hinzufügen eines Puffers zu diesem Kanal kann ein gewisser Spielraum geschaffen werden, um Verzögerungen bei der Ausführung zu verhindern, die durch die Auswirkungen nachgelagerter Aufgaben entstehen.
select
Wenn Sie mit mehreren Kanälen arbeiten, können Sie mit der select
-Syntax einfach eine fan-in
-Struktur implementieren.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch1 := make(chan int, 10)
10 ch2 := make(chan int, 10)
11 ch3 := make(chan int, 10)
12
13 go func() {
14 for {
15 ch1 <- 1
16 time.Sleep(1 * time.Second)
17 }
18 }()
19 go func() {
20 for {
21 ch2 <- 2
22 time.Sleep(2 * time.Second)
23 }
24 }()
25 go func() {
26 for {
27 ch3 <- 3
28 time.Sleep(3 * time.Second)
29 }
30 }()
31
32 for i := 0; i < 3; i++ {
33 select {
34 case v := <-ch1:
35 fmt.Println(v)
36 case v := <-ch2:
37 fmt.Println(v)
38 case v := <-ch3:
39 fmt.Println(v)
40 }
41 }
42}
Der obige Code erstellt drei Kanäle, die periodisch 1, 2 und 3 übermitteln, und gibt mit select
Werte aus den Kanälen aus. Durch die Verwendung von select
auf diese Weise können Sie Daten gleichzeitig von mehreren Kanälen empfangen und sie nach dem Empfang von Daten aus den Kanälen verarbeiten.
for range
Ein channel
kann mit for range
einfach Daten empfangen. Wenn for range
für einen Kanal verwendet wird, wird er jedes Mal ausgeführt, wenn Daten zu diesem Kanal hinzugefügt werden, und die Schleife wird beendet, wenn der Kanal geschlossen wird.
1package main
2
3func main() {
4 ch := make(chan int)
5 go func() {
6 ch <- 1
7 ch <- 2
8 close(ch)
9 }()
10
11 for i := range ch {
12 fmt.Println(i)
13 }
14}
Der obige Code ist ein Code, der channel
verwendet, um 1 und 2 auszugeben. Dieser Code verwendet for range
, um Daten jedes Mal zu empfangen und auszugeben, wenn dem Kanal Daten hinzugefügt werden. Und die Schleife wird beendet, wenn der Kanal geschlossen wird.
Wie bereits mehrfach erwähnt, kann diese Syntax auch für einfache Synchronisierungszwecke verwendet werden.
1package main
2
3func main() {
4 ch := make(chan struct{})
5 go func() {
6 defer close(ch)
7 time.Sleep(1 * time.Second)
8 fmt.Println("Hello, World!")
9 }()
10
11 fmt.Println("Waiting for goroutine...")
12 for range ch {}
13}
Der obige Code ist ein Code, der eine Sekunde wartet und dann Hello, World!
ausgibt. Dieser Code hat channel
verwendet, um synchronen Code in asynchronen Code zu ändern. Durch die Verwendung von channel
auf diese Weise können Sie synchronen Code einfach in asynchronen Code ändern und den join
-Punkt setzen.
etc.
- Wenn Sie Daten an einen nil-Kanal senden oder von ihm empfangen, können Sie in eine Endlosschleife geraten und einen Deadlock verursachen.
- Wenn Sie nach dem Schließen eines Kanals Daten senden, tritt eine Panik auf.
- Auch wenn Sie den Kanal nicht unbedingt schließen, schließt der GC den Kanal beim Sammeln.
mutex
Spinlock
Ein spinlock
ist eine Synchronisierungsmethode, die in einer Schleife wiederholt versucht, eine Sperre zu erhalten. In der Go-Sprache können wir einen Spinlock einfach mit einem Zeiger implementieren.
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) {
14 runtime.Gosched()
15 }
16}
17
18func (s *SpinLock) Unlock() {
19 atomic.StoreUintptr(&s.lock, 0)
20}
21
22func NewSpinLock() *SpinLock {
23 return &SpinLock{}
24}
Der obige Code ist ein Code, der das Paket spinlock
implementiert. Dieser Code verwendet das Paket sync/atomic
, um SpinLock
zu implementieren. Die Lock
-Methode verwendet atomic.CompareAndSwapUintptr
, um zu versuchen, eine Sperre zu erhalten, und die Unlock
-Methode verwendet atomic.StoreUintptr
, um die Sperre freizugeben. Da diese Methode ununterbrochen versucht, eine Sperre zu erhalten, verbraucht sie kontinuierlich die CPU, bis die Sperre erhalten wird, und kann daher in eine Endlosschleife geraten. Daher ist es ratsam, einen spinlock
für eine einfache Synchronisation oder nur für kurze Zeit zu verwenden.
sync.Mutex
Ein mutex
ist ein Werkzeug für die Synchronisation zwischen Goroutinen. Das von sync
Paket bereitgestellte mutex
bietet Methoden wie Lock
, Unlock
, RLock
und RUnlock
. Ein mutex
kann mit sync.Mutex
erstellt werden, und ein Lese-/Schreib-Lock kann auch mit sync.RWMutex
verwendet werden.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 var mu sync.Mutex
9 var count int
10
11 go func() {
12 mu.Lock()
13 count++
14 mu.Unlock()
15 }()
16
17 mu.Lock()
18 count++
19 mu.Unlock()
20
21 println(count)
22}
Im obigen Code greifen zwei Goroutinen fast gleichzeitig auf dieselbe Variable count
zu. Wenn Sie in diesem Fall den Code, der auf die Variable count
zugreift, mit einem mutex
in einen kritischen Bereich verwandeln, können Sie den gleichzeitigen Zugriff auf die Variable count
verhindern. Dann gibt dieser Code immer 2
aus, egal wie oft er ausgeführt wird.
sync.RWMutex
sync.RWMutex
ist ein mutex
, das es ermöglicht, Lese- und Schreibsperren separat zu verwenden. Sie können die Lesesperre mit den Methoden RLock
und RUnlock
setzen und freigeben.
1package cmap
2
3import (
4 "sync"
5)
6
7type ConcurrentMap[K comparable, V any] struct {
8 sync.RWMutex
9 data map[K]V
10}
11
12func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
13 m.RLock()
14 defer m.RUnlock()
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()
22 defer m.Unlock()
23
24 m.data[key] = value
25}
Der obige Code ist ein Code, der ConcurrentMap
mit sync.RWMutex
implementiert. Dieser Code verwendet eine Lesesperre in der Get
-Methode und eine Schreibsperre in der Set
-Methode, um sicher auf die data
-Map zuzugreifen und sie zu ändern. Der Grund, warum eine Lesesperre erforderlich ist, ist, dass, wenn viele einfache Leseoperationen vorhanden sind, mehrere Goroutinen gleichzeitig Leseoperationen ausführen können, indem nur Lesesperren und keine Schreibsperren gesetzt werden. Dadurch kann die Leistung verbessert werden, indem nur eine Lesesperre gesetzt wird, wenn keine Zustandsänderung erforderlich ist und keine Schreibsperre gesetzt werden muss.
fakelock
fakelock
ist ein einfacher Trick, um sync.Locker
zu implementieren. Diese Struktur bietet die gleichen Methoden wie sync.Mutex
, führt aber keine tatsächlichen Operationen aus.
1package fakelock
2
3type FakeLock struct{}
4
5func (f *FakeLock) Lock() {}
6
7func (f *FakeLock) Unlock() {}
Der obige Code ist ein Code, der das Paket fakelock
implementiert. Dieses Paket implementiert sync.Locker
und bietet die Methoden Lock
und Unlock
, führt aber in Wirklichkeit keine Operationen aus. Ich werde schreiben, warum dieser Code benötigt wird, wenn sich die Gelegenheit ergibt.
waitgroup
sync.WaitGroup
sync.WaitGroup
ist ein Werkzeug, um zu warten, bis alle Goroutinen abgeschlossen sind. Es bietet die Methoden Add
, Done
und Wait
. Die Anzahl der Goroutinen wird mit der Methode Add
hinzugefügt, und es wird mit der Methode Done
mitgeteilt, dass die Arbeit der Goroutine abgeschlossen ist. Und es wartet mit der Wait
-Methode, bis die Arbeit aller Goroutinen abgeschlossen ist.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{}
10 c := atomic.Int64{}
for i := 0; i < 100 ; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Add(1)
}()
}
wg.Wait()
println(c.Load())
}
1
2Dieser Code verwendet `sync.WaitGroup`, um 100 Go-Routinen gleichzeitig den Wert zur Variablen `c` hinzufügen zu lassen. In diesem Code wird `sync.WaitGroup` verwendet, um zu warten, bis alle Go-Routinen beendet sind, und dann der Wert, der der Variablen `c` hinzugefügt wurde, ausgegeben. Es ist ausreichend, nur Kanäle zu verwenden, wenn man einfach einige wenige Aufgaben mit `fork & join` ausführt, aber es ist eine gute Wahl, `sync.WaitGroup` zu verwenden, wenn man eine große Anzahl von Aufgaben mit `fork & join` ausführt.
3
4#### mit Slice
5
6In Verbindung mit einem Slice kann `waitgroup` ein hervorragendes Werkzeug sein, um gleichzeitige Ausführungen ohne Locks zu verwalten.
7
8```go
9package main
10
11import (
12 "fmt"
13 "sync"
14 "rand"
15)
16
17func main() {
18 var wg sync.WaitGroup
19 arr := [10]int{}
20
21 for i := 0; i < 10; i++ {
22 wg.Add(1)
23 go func(id int) {
24 defer wg.Done()
25
26 arr[id] = rand.Intn(100)
27 }(i)
28 }
29
30 wg.Wait()
31 fmt.Println("Fertig")
32
33 for i, v := range arr {
34 fmt.Printf("arr[%d] = %d\n", i, v)
35 }
36}
Der obige Code ist ein Code, der waitgroup
verwendet, um jede Go-Routine gleichzeitig 10 zufällige ganze Zahlen generieren und in dem zugewiesenen Index speichern zu lassen. In diesem Code wird waitgroup
verwendet, um zu warten, bis alle Go-Routinen abgeschlossen sind, und dann Fertig
auszugeben. Mit dieser Methode können mit waitgroup
mehrere Go-Routinen gleichzeitig Aufgaben ausführen, ohne Locks Daten speichern und nach Abschluss der Aufgaben eine Batch-Nachbearbeitung durchführen.
golang.org/x/sync/errgroup.ErrGroup
errgroup
ist ein Paket, das sync.WaitGroup
erweitert. Im Gegensatz zu sync.WaitGroup
bricht errgroup
alle Go-Routinen ab und gibt einen Fehler zurück, wenn bei einer der Go-Routinen ein Fehler auftritt.
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())
11 _ = ctx
12
13 for i := 0; i < 10; i++ {
14 i := i
15 g.Go(func() error {
16 if i == 5 {
17 return fmt.Errorf("error")
18 }
19 return nil
20 })
21 }
22
23 if err := g.Wait(); err != nil {
24 fmt.Println(err)
25 }
26}
Der obige Code ist ein Code, der errgroup
verwendet, um 10 Go-Routinen zu erzeugen und einen Fehler in der 5. Go-Routine zu verursachen. Ich habe absichtlich einen Fehler in der fünften Go-Routine verursacht, um zu zeigen, was passiert, wenn ein Fehler auftritt. In der tatsächlichen Verwendung sollte man jedoch errgroup
verwenden, um Go-Routinen zu erzeugen, und dann verschiedene Nachbearbeitungen durchführen, wenn in jeder Go-Routine ein Fehler auftritt.
Once
Dies ist ein Tool, um Code auszuführen, der nur einmal ausgeführt werden muss. Der zugehörige Code kann über die folgenden Konstruktoren ausgeführt werden.
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
sorgt einfach dafür, dass eine Funktion nur genau einmal im gesamten Programmablauf ausgeführt werden kann.
1package main
2
3import "sync"
4
5func main() {
6 once := sync.OnceFunc(func() {
7 println("Hallo, Welt!")
8 })
9
10 once()
11 once()
12 once()
13 once()
14 once()
15}
Der obige Code ist ein Code, der sync.OnceFunc
verwendet, um Hallo, Welt!
auszugeben. In diesem Code wird sync.OnceFunc
verwendet, um die Funktion once
zu erzeugen, und auch wenn die Funktion once
mehrmals aufgerufen wird, wird Hallo, Welt!
nur einmal ausgegeben.
OnceValue
OnceValue
sorgt nicht nur dafür, dass eine Funktion nur genau einmal im gesamten Programmablauf ausgeführt wird, sondern speichert auch den Rückgabewert der Funktion und gibt den gespeicherten Wert zurück, wenn er erneut aufgerufen wird.
1package main
2
3import "sync"
4
5func main() {
6 c := 0
7 once := sync.OnceValue(func() int {
8 c += 1
9 return c
10 })
11
12 println(once())
13 println(once())
14 println(once())
15 println(once())
16 println(once())
17}
Der obige Code ist ein Code, der sync.OnceValue
verwendet, um die Variable c
um 1 zu erhöhen. In diesem Code wird sync.OnceValue
verwendet, um die Funktion once
zu erzeugen, und auch wenn die Funktion once
mehrmals aufgerufen wird, gibt sie nur 1 zurück, da die Variable c
nur einmal erhöht wurde.
OnceValues
OnceValues
funktioniert genauso wie OnceValue
, kann aber mehrere Werte zurückgeben.
1package main
2
3import "sync"
4
5func main() {
6 c := 0
7 once := sync.OnceValues(func() (int, int) {
8 c += 1
9 return c, c
10 })
11
12 a, b := once()
13 println(a, b)
14 a, b = once()
15 println(a, b)
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}
Der obige Code ist ein Code, der sync.OnceValues
verwendet, um die Variable c
um 1 zu erhöhen. In diesem Code wird sync.OnceValues
verwendet, um die Funktion once
zu erzeugen, und auch wenn die Funktion once
mehrmals aufgerufen wird, gibt sie nur 1 zurück, da die Variable c
nur einmal erhöht wurde.
atomic
Das atomic
-Paket ist ein Paket, das atomare Operationen bereitstellt. Das atomic
-Paket bietet Methoden wie Add
, CompareAndSwap
, Load
, Store
, Swap
, aber die Verwendung von Typen wie Int64
, Uint64
, Pointer
wird in letzter Zeit empfohlen.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{}
10 c := atomic.Int64{}
11
12 for i := 0; i < 100 ; i++ {
13 wg.Add(1)
14 go func() {
15 defer wg.Done()
16 c.Add(1)
17 }()
18 }
19
20 wg.Wait()
21 println(c.Load())
22}
Dies ist das zuvor verwendete Beispiel. Es ist ein Code, der die Variable c
atomar mit dem Typ atomic.Int64
erhöht. Mit den Methoden Add
und Load
kann man eine Variable atomar erhöhen und die Variable einlesen. Außerdem kann man mit der Methode Store
einen Wert speichern, mit der Methode Swap
einen Wert austauschen und mit der Methode CompareAndSwap
einen Wert vergleichen und gegebenenfalls austauschen.
cond
sync.Cond
Das cond
-Paket ist ein Paket, das Bedingungsvariablen bereitstellt. Das cond
-Paket kann mit sync.Cond
erzeugt werden und bietet die Methoden Wait
, Signal
, Broadcast
.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 c := sync.NewCond(&sync.Mutex{})
9 ready := false
10
11 go func() {
12 c.L.Lock()
13 ready = true
14 c.Signal()
15 c.L.Unlock()
16 }()
17
18 c.L.Lock()
19 for !ready {
20 c.Wait()
21 }
22 c.L.Unlock()
23
24 println("Fertig!")
25}
Der obige Code ist ein Code, der sync.Cond
verwendet, um zu warten, bis die Variable ready
true
wird. In diesem Code wird sync.Cond
verwendet, um zu warten, bis die Variable ready
true
wird, und dann Fertig!
auszugeben. Auf diese Weise kann man mit sync.Cond
mehrere Go-Routinen gleichzeitig warten lassen, bis eine bestimmte Bedingung erfüllt ist.
Dies kann verwendet werden, um eine einfache queue
zu implementieren.
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
15 Cond *sync.Cond
16 Head *Node[T]
17 Tail *Node[T]
18 Len int
19}
20
21func New[T any]() *Queue[T] {
22 q := &Queue[T]{}
23 q.Cond = sync.NewCond(&q.Mutex)
24 return q
25}
26
27func (q *Queue[T]) Push(value T) {
28 q.Lock()
29 defer q.Unlock()
30
31 node := &Node[T]{Value: value}
32 if q.Len == 0 {
33 q.Head = node
34 q.Tail = node
35 } else {
36 q.Tail.Next = node
37 q.Tail = node
38 }
39 q.Len++
40 q.Cond.Signal()
41}
42
43func (q *Queue[T]) Pop() T {
44 q.Lock()
45 defer q.Unlock()
46
47 for q.Len == 0 {
48 q.Cond.Wait()
49 }
50
51 node := q.Head
52 q.Head = q.Head.Next
53 q.Len--
54 return node.Value
55}
Mit sync.Cond
kann man effizient warten und, sobald die Bedingung erfüllt ist, wieder agieren, anstatt durch spin-lock
eine hohe CPU-Auslastung zu verursachen.
semaphore
golang.org/x/sync/semaphore.Semaphore
Das semaphore
-Paket ist ein Paket, das Semaphore bereitstellt. Das semaphore
-Paket kann mit golang.org/x/sync/semaphore.Semaphore
erzeugt werden und bietet die Methoden Acquire
, Release
, TryAcquire
.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/semaphore"
7)
8
9func main() {
10 s := semaphore.NewWeighted(1)
11
12 if s.TryAcquire(1) {
13 fmt.Println("Erworben!")
14 } else {
15 fmt.Println("Nicht erworben!")
16 }
17
18 s.Release(1)
19}
Der obige Code ist ein Code, der mit semaphore
ein Semaphor erzeugt und mit dem Semaphor über die Methode Acquire
ein Semaphor erwirbt und mit der Methode Release
ein Semaphor freigibt. In diesem Code wurde gezeigt, wie man mit semaphore
ein Semaphor erwirbt und freigibt.
Abschließend
Das sind die grundlegenden Inhalte. Basierend auf dem Inhalt dieses Artikels hoffe ich, dass Sie verstehen, wie man mit Go-Routinen die Nebenläufigkeit verwaltet und es tatsächlich verwenden können. Ich hoffe, dass dieser Artikel Ihnen geholfen hat. Vielen Dank.