GoSuda

Go-Gleichzeitigkeits-Starterpaket

By snowmerak
views ...

Ü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.

  1. Wenn Sie Daten an einen nil-Kanal senden oder von ihm empfangen, können Sie in eine Endlosschleife geraten und einen Deadlock verursachen.
  2. Wenn Sie nach dem Schließen eines Kanals Daten senden, tritt eine Panik auf.
  3. 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.