Go Concurrency Starterpaket
Überblick
Kurze Einführung
Die Sprache Go bietet zahlreiche Werkzeuge zur Verwaltung von Parallelität. In diesem Artikel werden wir einige davon sowie nützliche Tricks vorstellen.
Goroutinen?
Goroutinen sind eine neue Art von Parallelitätsmodell, die von der Sprache Go unterstützt wird. Im Allgemeinen empfängt ein Programm OS-Threads vom Betriebssystem, um mehrere Aufgaben gleichzeitig auszuführen, und führt die Aufgaben parallel in Abhängigkeit von der Anzahl der Cores aus. Um Parallelität in kleineren Einheiten zu ermöglichen, werden im Userland Green Threads erstellt, die innerhalb eines einzelnen OS-Threads arbeiten. Im Falle von Goroutinen wurden diese Green Threads jedoch noch kleiner und effizienter gestaltet. Goroutinen verbrauchen weniger Speicher als Threads und können schneller erstellt und ausgetauscht werden als Threads.
Um Goroutinen zu verwenden, genügt es, das Schlüsselwort go zu nutzen. Dies ermöglicht es, synchronen Code während des Programmierens intuitiv asynchron 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 wandelt einen synchronen Code, der einfach 1 Sekunde wartet und dann Hello, World! ausgibt, in einen asynchronen Ablauf um. Das aktuelle Beispiel ist einfach, aber wenn etwas komplexerer synchroner Code in asynchronen Code umgewandelt wird, verbessern sich die Lesbarkeit, Sichtbarkeit und Verständlichkeit des Codes im Vergleich zu bestehenden Methoden wie async/await oder Promises erheblich.
Oftmals führt jedoch ein mangelndes Verständnis des Ablaufs, bei dem synchroner Code einfach asynchron aufgerufen wird, und des fork & join-Ablaufs (ähnlich einer Divide-and-Conquer-Strategie) zur Erstellung schlechten Goroutinen-Codes. Dieser Artikel wird einige Methoden und Techniken vorstellen, um solchen Situationen vorzubeugen.
Parallelitätsverwaltung
context
Dass context als erste Verwaltungstechnik erscheint, mag überraschend sein. Doch in Go spielt context eine herausragende Rolle bei der Verwaltung des gesamten Aufgabenbaums, weit über eine einfache Abbruchfunktion hinaus. Für diejenigen, die es nicht kennen, werde ich das 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 verwendet context, um nach 1 Sekunde Context is done! auszugeben. context kann den Abbruchstatus über die Methode Done() überprüfen und bietet verschiedene Abbruchmethoden über Methoden wie WithCancel, WithTimeout, WithDeadline und WithValue.
Erstellen wir ein einfaches Beispiel. Angenommen, Sie schreiben Code, der unter Verwendung des aggregator-Musters user, post und comment abruft, um bestimmte Daten zu erhalten. Und alle Anfragen müssen innerhalb von 2 Sekunden erfolgen, dann können Sie es 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 "All data is fetched!" wenn alle Daten abgerufen wurden. Auf diese Weise können Sie mit context den Abbruch und Timeout auch in Code, in dem mehrere Goroutinen laufen, einfach verwalten.
Verwandte Funktionen und Methoden zu context finden Sie unter godoc context. Es wäre wünschenswert, dass Sie die grundlegenden Konzepte lernen und bequem nutzen können.
channel
unbuffered channel
Ein channel ist ein Werkzeug zur Kommunikation zwischen Goroutinen. Ein channel kann mit make(chan T) erstellt werden. Hierbei ist T der Datentyp, den der channel übertragen wird. Daten können über <- gesendet und empfangen werden, und der 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 gibt 1 und 2 unter Verwendung eines channel aus. Dieser Code zeigt lediglich das Senden und Empfangen von Werten über einen channel. Ein channel bietet jedoch noch viele weitere Funktionen. Zuerst werden wir uns mit buffered channel und unbuffered channel befassen. Vorab ist das oben geschriebene Beispiel ein unbuffered channel, bei dem das Senden von Daten an den Kanal und das Empfangen von Daten gleichzeitig erfolgen müssen. Wenn diese Aktionen nicht gleichzeitig stattfinden, kann es zu einem Deadlock kommen.
buffered channel
Was wäre, wenn der obige Code nicht nur eine einfache Ausgabe wäre, sondern zwei Prozesse, die schwere Aufgaben ausführen? Wenn der zweite Prozess beim Lesen und Verarbeiten über längere Zeit blockiert wäre, würde auch der erste Prozess für die gleiche Zeitspanne anhalten. Um solche Situationen zu vermeiden, können wir einen buffered channel verwenden.
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 gibt 1 und 2 unter Verwendung eines buffered channel aus. In diesem Code wurde ein buffered channel verwendet, um zu ermöglichen, dass das Senden von Daten an den channel und das Empfangen von Daten nicht gleichzeitig erfolgen müssen. Wenn ein Puffer in den Kanal gelegt wird, entsteht ein Spielraum in der entsprechenden Länge, wodurch Arbeitsverzögerungen, die durch nachrangige Aufgaben verursacht werden, vermieden werden können.
select
Beim Umgang mit mehreren Kanälen kann die select-Anweisung verwendet werden, um eine fan-in-Struktur einfach zu implementieren.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8)
9
10func main() {
11 ch1 := make(chan int, 10)
12 ch2 := make(chan int, 10)
13 ch3 := make(chan int, 10)
14
15 go func() {
16 for {
17 ch1 <- 1
18 time.Sleep(1 * time.Second)
19 }
20 }()
21 go func() {
22 for {
23 ch2 <- 2
24 time.Sleep(2 * time.Second)
25 }
26 }()
27 go func() {
28 for {
29 ch3 <- 3
30 time.Sleep(3 * time.Second)
31 }
32 }()
33
34 for i := 0; i < 3; i++ {
35 select {
36 case v := <-ch1:
37 fmt.Println(v)
38 case v := <-ch2:
39 fmt.Println(v)
40 case v := <-ch3:
41 fmt.Println(v)
42 }
43 }
44}
Der obige Code erstellt drei Kanäle, die periodisch 1, 2, 3 senden, und verwendet select, um Werte von den Kanälen zu empfangen und auszugeben. Auf diese Weise ermöglicht select das gleichzeitige Empfangen von Daten von mehreren Kanälen und deren Verarbeitung, sobald sie empfangen werden.
for range
Ein channel kann einfach Daten über for range empfangen. Wenn for range auf einen Kanal angewendet wird, wird es 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 gibt 1 und 2 unter Verwendung eines channel aus. In diesem Code werden Daten jedes Mal, wenn sie dem Kanal hinzugefügt werden, über for range empfangen und ausgegeben. Und die Schleife wird beendet, wenn der Kanal geschlossen wird.
Wie bereits mehrfach erwähnt, kann diese Syntax auch für einfache Synchronisationsmittel 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 gibt nach 1 Sekunde Hello, World! aus. In diesem Code wurde ein channel verwendet, um synchronen Code in asynchronen Code umzuwandeln. Auf diese Weise können Sie mit channel synchronen Code einfach in asynchronen Code umwandeln und einen join-Punkt festlegen.
etc
- Wenn Daten an einen nil channel gesendet oder von ihm empfangen werden, kann es zu einer Endlosschleife und einem Deadlock kommen.
- Wenn nach dem Schließen eines Kanals Daten gesendet werden, tritt ein panic auf.
- Auch wenn ein Kanal nicht explizit geschlossen wird, schließt ihn der GC beim Aufräumen.
mutex
spinlock
Ein spinlock ist eine Synchronisationsmethode, die kontinuierlich versucht, eine Sperre in einer Schleife zu erlangen. In Go kann man Spinlocks einfach mit Pointern 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 implementiert das spinlock-Paket. In diesem Code wurde SpinLock unter Verwendung des sync/atomic-Pakets implementiert. Die Lock-Methode versucht, die Sperre mit atomic.CompareAndSwapUintptr zu erhalten, und die Unlock-Methode löst die Sperre mit atomic.StoreUintptr. Da diese Methode ununterbrochen versucht, die Sperre zu erhalten, verbraucht sie kontinuierlich CPU-Ressourcen, bis die Sperre erlangt ist, was zu einer Endlosschleife führen kann. Daher sollte spinlock nur für einfache Synchronisationen oder für kurze Zeiträume verwendet werden.
sync.Mutex
Ein mutex ist ein Werkzeug zur Synchronisation zwischen Goroutinen. Der im sync-Paket bereitgestellte mutex bietet Methoden wie Lock, Unlock, RLock und RUnlock. Ein mutex kann mit sync.Mutex erstellt werden, und sync.RWMutex kann für Lese-/Schreibsperren 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 nahezu gleichzeitig auf die gleiche Variable count zu. Durch die Verwendung eines mutex kann der Code, der auf die Variable count zugreift, als kritischer Bereich definiert werden, wodurch der gleichzeitige Zugriff auf die Variable count verhindert wird. Dann wird dieser Code, egal wie oft er ausgeführt wird, immer 2 ausgeben.
sync.RWMutex
sync.RWMutex ist ein mutex, der Lese- und Schreibsperren getrennt verwenden kann. Die Methoden RLock und RUnlock können verwendet werden, um eine Lesesperre zu setzen und aufzuheben.
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 implementiert ConcurrentMap unter Verwendung von sync.RWMutex. In diesem Code wird eine Lesesperre in der Get-Methode und eine Schreibsperre in der Set-Methode gesetzt, um sicher auf die data-Map zuzugreifen und sie zu ändern. Der Grund für die Notwendigkeit einer Lesesperre ist, dass bei vielen einfachen Leseoperationen mehrere Goroutinen gleichzeitig Leseoperationen ausführen können, ohne eine Schreibsperre zu setzen, sondern nur eine Lesesperre. Dadurch kann die Leistung verbessert werden, indem nur eine Lesesperre gesetzt wird, wenn keine Zustandsänderung erforderlich ist und somit keine Schreibsperre gesetzt werden muss.
fakelock
fakelock ist ein einfacher Trick, der sync.Locker implementiert. 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 implementiert das fakelock-Paket. Dieses Paket implementiert sync.Locker und bietet die Methoden Lock und Unlock, führt aber tatsächlich keine Operationen aus. Warum dieser Code notwendig ist, werde ich bei Gelegenheit erläutern.
waitgroup
sync.WaitGroup
sync.WaitGroup ist ein Werkzeug, das darauf wartet, dass alle Goroutinen ihre Arbeit abgeschlossen haben. Es bietet die Methoden Add, Done und Wait. Die Methode Add fügt die Anzahl der Goroutinen hinzu, und die Methode Done signalisiert, dass die Arbeit einer Goroutine abgeschlossen ist. Die Methode Wait wartet dann, bis alle Goroutinen ihre Arbeit beendet haben.
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}
Der obige Code verwendet sync.WaitGroup, um 100 Goroutinen zu ermöglichen, gleichzeitig Werte zur Variablen c zu addieren. In diesem Code wird sync.WaitGroup verwendet, um zu warten, bis alle Goroutinen abgeschlossen sind, und dann der zur Variablen c addierte Wert ausgegeben. Für die einfache fork & join-Operation einiger Aufgaben ist ein Kanal ausreichend, aber für eine große Anzahl von fork & join-Operationen ist sync.WaitGroup eine gute Option.
with slice
In Kombination mit Slices kann waitgroup ein hervorragendes Werkzeug zur Verwaltung paralleler Aufgaben ohne Locks sein.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "rand"
7)
8
9func main() {
10 var wg sync.WaitGroup
11 arr := [10]int{}
12
13 for i := 0; i < 10; i++ {
14 wg.Add(1)
15 go func(id int) {
16 defer wg.Done()
17
18 arr[id] = rand.Intn(100)
19 }(i)
20 }
21
22 wg.Wait()
23 fmt.Println("Done")
24
25 for i, v := range arr {
26 fmt.Printf("arr[%d] = %d\n", i, v)
27 }
28}
Der obige Code verwendet nur waitgroup, um jede Goroutine gleichzeitig 10 zufällige Ganzzahlen generieren und im zugewiesenen Index speichern zu lassen. Dieser Code verwendet waitgroup, um zu warten, bis alle Goroutinen abgeschlossen sind, und gibt dann Done aus. Auf diese Weise können Sie mit waitgroup mehrere Goroutinen gleichzeitig ausführen lassen, Daten ohne Sperre speichern, bis alle Goroutinen abgeschlossen sind, und nach Abschluss der Arbeit eine Stapelverarbeitung durchführen.
golang.org/x/sync/errgroup.ErrGroup
errgroup ist ein erweitertes Paket von sync.WaitGroup. Im Gegensatz zu sync.WaitGroup gibt errgroup` einen Fehler zurück und bricht alle Goroutinen ab, wenn auch nur eine Goroutine einen Fehler aufweist.
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 verwendet errgroup, um 10 Goroutinen zu erstellen und einen Fehler in der 5. Goroutine auszulösen. Ich habe absichtlich einen Fehler in der fünften Goroutine ausgelöst, um den Fall zu zeigen, in dem ein Fehler auftritt. Bei der tatsächlichen Verwendung wird errgroup verwendet, um Goroutinen zu erstellen, und es werden verschiedene Nachbearbeitungen durchgeführt, wenn in jeder Goroutine ein Fehler auftritt.
once
Dies ist ein Werkzeug, das Code ausführt, der nur einmal ausgeführt werden soll. 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 lediglich dafür, dass die betreffende Funktion über die gesamte Ausführung hinweg genau einmal ausgeführt wird.
1package main
2
3import "sync"
4
5func main() {
6 once := sync.OnceFunc(func() {
7 println("Hello, World!")
8 })
9
10 once()
11 once()
12 once()
13 once()
14 once()
15}
Der obige Code gibt Hello, World! unter Verwendung von sync.OnceFunc aus. In diesem Code wird die once-Funktion unter Verwendung von sync.OnceFunc erstellt, und selbst wenn die once-Funktion mehrmals aufgerufen wird, wird Hello, World! nur einmal ausgegeben.
OnceValue
OnceValue sorgt nicht nur dafür, dass die Funktion über die gesamte Ausführung hinweg nur einmal ausgeführt wird, sondern speichert auch den Rückgabewert dieser Funktion und gibt den gespeicherten Wert zurück, wenn sie 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 inkrementiert die Variable c um 1 unter Verwendung von sync.OnceValue. In diesem Code wird die Funktion once unter Verwendung von sync.OnceValue erstellt, und selbst wenn die Funktion once mehrmals aufgerufen wird, gibt sie den Wert 1 zurück, der nur einmal erhöht wurde.
OnceValues
OnceValues funktioniert 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 inkrementiert die Variable c um 1 unter Verwendung von sync.OnceValues. In diesem Code wird die Funktion once unter Verwendung von sync.OnceValues erstellt, und selbst wenn die Funktion once mehrmals aufgerufen wird, gibt sie den Wert 1 zurück, der nur einmal erhöht wurde.
atomic
Das atomic-Paket stellt atomare Operationen bereit. Das atomic-Paket bietet Methoden wie Add, CompareAndSwap, Load, Store und Swap, aber in letzter Zeit wird die Verwendung von Typen wie Int64, Uint64 und Pointer 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 inkrementiert. Mit den Methoden Add und Load können Variablen atomar inkrementiert und gelesen werden. Außerdem können Werte mit der Methode Store gespeichert, mit der Methode Swap ausgetauscht und mit der Methode CompareAndSwap verglichen und bei Eignung ausgetauscht werden.
cond
sync.Cond
Das cond-Paket stellt Bedingungsvariablen bereit. Das cond-Paket kann mit sync.Cond erstellt werden und bietet die Methoden Wait, Signal und 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("Ready!")
25}
Der obige Code verwendet sync.Cond, 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 Ready! ausgegeben. Auf diese Weise können Sie mit sync.Cond mehrere Goroutinen gleichzeitig warten lassen, bis eine bestimmte Bedingung erfüllt ist.
Dies kann genutzt 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}
Durch die Nutzung von sync.Cond können Sie anstatt einer spin-lock, die viel CPU-Auslastung verursacht, effizient warten und die Operation fortsetzen, sobald die Bedingung erfüllt ist.
semaphore
golang.org/x/sync/semaphore.Semaphore
Das semaphore-Paket stellt Semaphoren bereit. Das semaphore-Paket kann mit golang.org/x/sync/semaphore.Semaphore erstellt werden und bietet die Methoden Acquire, Release und 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("Acquired!")
14 } else {
15 fmt.Println("Not Acquired!")
16 }
17
18 s.Release(1)
19}
Der obige Code erstellt eine Semaphore mit dem semaphore-Paket und zeigt, wie man die Semaphore mit der Acquire-Methode erwirbt und mit der Release-Methode freigibt. In diesem Code habe ich gezeigt, wie man eine Semaphore erwirbt und freigibt.
Fazit
Ich denke, die Grundlagen sind hier ausreichend behandelt. Basierend auf dem Inhalt dieses Artikels hoffe ich, dass Sie die Methoden zur Verwaltung von Parallelität mit Goroutinen verstehen und praktisch anwenden können. Ich hoffe, dieser Artikel war Ihnen hilfreich. Vielen Dank.