GoSuda

Go-Starterpaket für Nebenläufigkeit

By snowmerak views ...

Überblick

Kurze Einführung

Die Sprache Go verfügt über viele Werkzeuge zur Verwaltung von Nebenläufigkeit. In diesem Artikel möchte ich einige davon sowie einige Tricks vorstellen.

Goroutinen?

Eine goroutine ist ein neuartiges Nebenläufigkeitsmodell, das von der Sprache Go unterstützt wird. Üblicherweise erhält ein Programm vom Betriebssystem OS-Threads, um mehrere Aufgaben gleichzeitig auszuführen, und arbeitet dabei parallel entsprechend der Anzahl der Kerne. Um Nebenläufigkeit auf einer feineren Ebene zu realisieren, erzeugt man im Userland grüne Threads, sodass innerhalb eines einzelnen OS-Threads mehrere grüne Threads abwechselnd Aufgaben ausführen. Goroutinen wurden jedoch als eine Form solcher grünen Threads entwickelt, die noch kleiner und effizienter sind. Goroutinen benötigen weniger Speicher als Threads und können schneller erzeugt und umgeschaltet werden.

Um Goroutinen zu verwenden, genügt es, einfach das Schlüsselwort go zu benutzen. Dadurch lässt sich bei der Programmierung intuitiv synchroner Code als asynchroner Code ausfü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 synchronen Code, der eine Sekunde wartet und anschließend Hello, World! ausgibt, in einen asynchronen Ablauf um. Das gegenwärtige Beispiel ist einfach, doch wenn man etwas komplexeren Code von synchron nach asynchron überführt, verbessern sich Lesbarkeit, Übersichtlichkeit und Verständlichkeit gegenüber Ansätzen wie async/await oder Promises.

In vielen Fällen entsteht jedoch schlechter Goroutinen-Code, wenn man den Ablauf, solchen synchronen Code einfach asynchron aufzurufen, sowie Abläufe wie fork & join (ähnlich wie bei Divide and Conquer) nicht versteht. Um solchen Fällen zu begegnen, stelle ich in diesem Artikel einige Methoden und Techniken vor.

Verwaltung von Nebenläufigkeit

context

Dass context als erstes Verwaltungswerkzeug erscheint, mag überraschen. In Go übernimmt context jedoch über die einfache Abbruchfunktion hinaus hervorragend die Verwaltung des gesamten Aufgabenbaums. Für diejenigen, die das Paket nicht kennen, erkläre ich es kurz.

 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 einer Sekunde Context is done! auszugeben. Über die Methode Done() lässt sich prüfen, ob abgebrochen wurde, und mit Methoden wie WithCancel, WithTimeout, WithDeadline und WithValue stehen verschiedene Abbruchmethoden zur Verfügung.

Lassen Sie uns ein einfaches Beispiel erstellen. Angenommen, Sie möchten mit dem Muster aggregator die Daten user, post und comment abrufen, und alle Anfragen müssen innerhalb von zwei Sekunden erfolgen. Dann können 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 zwei Sekunden abgerufen werden konnten, und All data is fetched!, wenn alle Daten abgerufen wurden. Auf diese Weise lassen sich Abbruch und Timeout auch in Code mit mehreren laufenden Goroutinen leicht verwalten, wenn man context verwendet.

Weitere Funktionen und Methoden im Zusammenhang mit context finden Sie in der godoc context. Ich hoffe, dass Sie das Einfache erlernen und bequem nutzen können.

channel

unbuffered channel

Ein channel ist ein Werkzeug zur Kommunikation zwischen Goroutinen. Ein channel kann mit make(chan T) erzeugt werden. Dabei ist T der Typ der Daten, die der jeweilige channel übertragen soll. Mit <- können Daten gesendet und empfangen werden, und mit close lässt sich ein channel schließen.

 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 mithilfe eines channel 1 und 2 aus. In diesem Code wird lediglich gezeigt, wie Werte an einen channel gesendet und von ihm empfangen werden. Ein channel bietet jedoch weit mehr Funktionen. Zunächst betrachten wir buffered channel und unbuffered channel. Vorab sei gesagt, dass das oben gezeigte Beispiel ein unbuffered channel ist, bei dem das Senden und das Empfangen von Daten gleichzeitig erfolgen müssen. Wenn diese Vorgänge nicht gleichzeitig stattfinden, kann ein Deadlock entstehen.

buffered channel

Wie wäre es, wenn der obige Code keine einfache Ausgabe, sondern zwei Prozesse mit aufwendigen Aufgaben wäre? Wenn der zweite Prozess beim Lesen und Verarbeiten längere Zeit blockiert, würde auch der erste Prozess in dieser Zeit angehalten. Um diese Situation 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 mithilfe eines buffered channel 1 und 2 aus. In diesem Code wird ein buffered channel verwendet, sodass das Senden und das Empfangen von Daten nicht gleichzeitig erfolgen müssen. Durch einen Puffer im Channel entsteht Spielraum in der entsprechenden Länge, wodurch sich Verzögerungen vermeiden lassen, die durch die Last nachrangiger Aufgaben entstehen.

select

Wenn Sie mit mehreren Kanälen arbeiten, lässt sich mit der Anweisung select leicht 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 erzeugt drei Kanäle, die periodisch 1, 2 und 3 übermitteln, und gibt die über select aus den Kanälen empfangenen Werte aus. Auf diese Weise können Sie mit select gleichzeitig Daten aus mehreren Kanälen empfangen und jeden Wert verarbeiten, sobald er eintrifft.

for range

Ein channel lässt sich mit for range leicht auslesen. Wird for range auf einen Kanal angewendet, wird es bei jedem Hinzufügen von Daten zu diesem Kanal ausgeführt, und die Schleife endet, sobald 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 mithilfe eines channel 1 und 2 aus. In diesem Code werden mit for range bei jedem Hinzufügen von Daten zum Kanal die Daten empfangen und ausgegeben. Wird der Kanal geschlossen, endet die Schleife.

Wie bereits mehrfach gezeigt, kann diese Syntax auch als einfaches 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 wartet eine Sekunde und gibt danach Hello, World! aus. In diesem Code wird ein channel verwendet, um synchronen Code in asynchronen Code umzuwandeln. Auf diese Weise lässt sich mit einem channel synchroner Code leicht in asynchronen Code überführen und ein join-Punkt festlegen.

etc

  1. Wenn man Daten an einen nil channel sendet oder von ihm empfängt, kann es zu einer Endlosschleife und somit zu einem Deadlock kommen.
  2. Wenn man nach dem Schließen eines Kanals Daten sendet, tritt ein panic auf.
  3. Auch wenn man einen Kanal nicht ausdrücklich schließt, schließt ihn der GC beim Einsammeln.

mutex

spinlock

Ein spinlock ist ein Synchronisationsverfahren, das in einer Schleife wiederholt versucht, die Sperre zu erlangen. In Go lässt sich mithilfe von Zeigern leicht ein Spinlock 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 eine Implementierung des Pakets spinlock. Hier wird SpinLock mithilfe des Pakets sync/atomic realisiert. In der Methode Lock wird mit atomic.CompareAndSwapUintptr versucht, die Sperre zu setzen, und in der Methode Unlock wird mit atomic.StoreUintptr die Sperre freigegeben. Da dieses Verfahren ununterbrochen versucht, die Sperre zu erlangen, verbraucht es bis zum Erhalt der Sperre fortlaufend CPU-Leistung und kann in eine Endlosschleife geraten. Daher ist es ratsam, spinlock nur für einfache Synchronisation oder für sehr kurze Zeiträume zu verwenden.

sync.Mutex

mutex ist ein Werkzeug zur Synchronisation zwischen Goroutinen. Das vom Paket sync bereitgestellte mutex bietet Methoden wie Lock, Unlock, RLock und RUnlock. Ein mutex kann als sync.Mutex erzeugt werden, und mit sync.RWMutex lässt sich außerdem eine Lese-/Schreibsperre verwenden.

 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 dieselbe Variable count zu. Wenn man den Code, der auf die Variable count zugreift, mithilfe eines mutex zu einem kritischen Abschnitt macht, lässt sich der gleichzeitige Zugriff auf count verhindern. Dann gibt dieser Code unabhängig von der Anzahl der Ausführungen stets 2 aus.

sync.RWMutex

sync.RWMutex ist ein mutex, bei dem Lesesperren und Schreibsperren unterschieden werden. Mit den Methoden RLock und RUnlock können Sie eine Lesesperre setzen und wieder aufheben.

 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 mithilfe von sync.RWMutex. In diesem Code wird in der Methode Get eine Lesesperre und in der Methode Set eine Schreibsperre gesetzt, sodass auf die Map data sicher zugegriffen und sie verändert werden kann. Die Lesesperre ist deshalb notwendig, weil bei überwiegend lesenden Operationen ohne Schreibsperre nur eine Lesesperre gesetzt wird, damit mehrere Goroutinen gleichzeitig lesen können. Dadurch lässt sich die Leistung verbessern, wenn keine Zustandsänderung erfolgt und somit keine Schreibsperre nötig ist.

fakelock

fakelock ist ein einfacher Trick, bei dem sync.Locker implementiert wird. Diese Struktur bietet dieselben Methoden wie sync.Mutex, führt jedoch tatsächlich keinerlei Aktion aus.

1package fakelock
2
3type FakeLock struct{}
4
5func (f *FakeLock) Lock() {}
6
7func (f *FakeLock) Unlock() {}

Der obige Code ist eine Implementierung des Pakets fakelock. Dieses Paket implementiert sync.Locker und stellt die Methoden Lock und Unlock bereit, führt jedoch tatsächlich keinerlei Operation aus. Warum dieser Code benötigt wird, werde ich bei Gelegenheit erläutern.

waitgroup

sync.WaitGroup

sync.WaitGroup ist ein Werkzeug, das wartet, bis alle Aufgaben von Goroutinen abgeschlossen sind. Es bietet die Methoden Add, Done und Wait. Mit der Methode Add wird die Anzahl der Goroutinen erhöht, mit Done wird signalisiert, dass die Aufgabe einer Goroutine beendet ist. Mit Wait wird gewartet, bis die Aufgaben aller Goroutinen abgeschlossen sind.

 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, damit 100 Goroutinen gleichzeitig einen Wert zur Variable c addieren. In diesem Code wartet sync.WaitGroup, bis alle Goroutinen abgeschlossen sind, und anschließend wird der in c addierte Wert ausgegeben. Bei einem einfachen fork & join einiger weniger Aufgaben genügt es, nur Channels zu verwenden, während bei einem fork & join einer großen Anzahl von Aufgaben sync.WaitGroup eine gute Wahl sein kann.

with slice

In Verbindung mit einem Slice kann waitgroup ein hervorragendes Werkzeug zur Verwaltung nebenläufiger Ausführung ohne Sperren werden.

 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 erzeugt mit waitgroup allein in jeder Goroutine gleichzeitig jeweils eine von 10 Zufallszahlen und speichert sie am zugewiesenen Index. In diesem Code wartet waitgroup, bis alle Goroutinen abgeschlossen sind, und gibt danach Done aus. Auf diese Weise können bei Verwendung von waitgroup mehrere Goroutinen gleichzeitig Aufgaben ausführen, Daten ohne Sperren speichern, bis alle Goroutinen beendet sind, und danach gesammelt nachbearbeitet werden.

golang.org/x/sync/errgroup.ErrGroup

errgroup ist ein Paket, das sync.WaitGroup erweitert. Im Gegensatz zu sync.WaitGroup bricht errgroup alle Goroutinen ab und gibt den Fehler zurück, sobald in einer der Aufgaben der Goroutinen 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 erzeugt mit errgroup 10 Goroutinen, wobei in der fünften Goroutine ein Fehler ausgelöst wird. Der Fehler in der fünften Goroutine wurde absichtlich ausgelöst, um den Fall eines Fehlers zu zeigen. In der tatsächlichen Anwendung erzeugt man Goroutinen mit errgroup und führt dann für den Fall, dass in einer Goroutine ein Fehler auftritt, verschiedene Nachbearbeitungen durch.

once

Ein Werkzeug, um Code auszuführen, der nur einmal ausgeführt werden darf. Über die folgenden Konstruktoren lässt sich der entsprechende Code ausführen.

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 die jeweilige Funktion über die gesamte Laufzeit hinweg genau einmal ausgeführt werden kann.

 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 mithilfe von sync.OnceFunc Hello, World! aus. In diesem Code wird mit sync.OnceFunc die Funktion once erzeugt, und obwohl once mehrfach aufgerufen wird, wird Hello, World! nur einmal ausgegeben.

OnceValue

OnceValue sorgt nicht einfach nur dafür, dass die jeweilige Funktion über die gesamte Laufzeit hinweg genau einmal ausgeführt wird, sondern speichert zusätzlich den Rückgabewert der Funktion und gibt bei erneuten Aufrufen den gespeicherten Wert zurück.

 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 erhöht mithilfe von sync.OnceValue die Variable c um jeweils 1. In diesem Code wird mit sync.OnceValue die Funktion once erzeugt, und obwohl once mehrfach aufgerufen wird, wird die Variable c nur einmal erhöht und liefert den Wert 1 zurück.

OnceValues

OnceValues funktioniert genauso wie OnceValue, kann jedoch 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 erhöht mithilfe von sync.OnceValues die Variable c um jeweils 1. In diesem Code wird mit sync.OnceValues die Funktion once erzeugt, und obwohl once mehrfach aufgerufen wird, wird die Variable c nur einmal erhöht und liefert den Wert 1 zurück.

atomic

Das Paket atomic stellt atomare Operationen bereit. Es bietet Methoden wie Add, CompareAndSwap, Load, Store und Swap, empfohlen wird jedoch in letzter Zeit die Verwendung von Typen wie Int64, Uint64 und Pointer.

 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 Beispiel, das wir zuvor verwendet haben. Es zeigt Code, der den Typ atomic.Int64 verwendet, um die Variable c atomar zu erhöhen. Mit den Methoden Add und Load lässt sich die Variable atomar erhöhen und auslesen. Außerdem kann mit der Methode Store ein Wert gespeichert, mit der Methode Swap ein Wert ausgetauscht und mit der Methode CompareAndSwap ein Wert verglichen und bei Übereinstimmung ersetzt werden.

cond

sync.Cond

Das Paket cond stellt Bedingungsvariablen bereit. Es lässt sich mit sync.Cond erzeugen 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 wartet mithilfe von sync.Cond, bis die Variable ready den Wert true annimmt. In diesem Code wird mit sync.Cond gewartet, bis die Variable ready true wird, und danach wird Ready! ausgegeben. Auf diese Weise lässt sich mit sync.Cond erreichen, dass mehrere Goroutinen gleichzeitig warten, bis eine bestimmte Bedingung erfüllt ist.

Darauf aufbauend lässt sich eine einfache queue 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}

Wenn man sync.Cond auf diese Weise nutzt, kann man effizient warten, statt mit einem spin-lock viel CPU-Last zu erzeugen, und wieder weiterarbeiten, sobald die Bedingung erfüllt ist.

semaphore

golang.org/x/sync/semaphore.Semaphore

Das Paket semaphore stellt Semaphoren bereit. Es lässt sich mit golang.org/x/sync/semaphore.Semaphore erzeugen 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 erzeugt mit semaphore eine Semaphore und zeigt, wie sie mit der Methode Acquire erworben und mit der Methode Release wieder freigegeben wird. In diesem Code wird gezeigt, wie man mit semaphore eine Semaphore erwirbt und wieder freigibt.

Abschließende Worte

Die grundlegenden Inhalte dürften hiermit ausreichen. Ich hoffe, dass Sie auf Grundlage dieses Artikels verstehen, wie man Nebenläufigkeit mithilfe von Goroutinen verwaltet, und es praktisch einsetzen können. Ich hoffe, dass dieser Artikel für Sie hilfreich war. Vielen Dank.