GoSuda

Go Concurrency Starter Pack

By snowmerak
views ...

Overzicht

Korte introductie

De Go-taal biedt diverse hulpmiddelen voor het beheren van gelijktijdigheid. In dit artikel introduceren wij u enkele van deze hulpmiddelen en bijbehorende technieken.

Goroutine?

Een goroutine is een nieuw type gelijktijdigheidsmodel dat wordt ondersteund in de Go-taal. Doorgaans ontvangt een programma OS-threads van het besturingssysteem om gelijktijdig meerdere taken uit te voeren, waarbij taken parallel worden uitgevoerd tot het aantal beschikbare kernen. Voor gelijktijdigheid op een kleinere schaal worden in de userland "groene threads" gecreëerd, zodat meerdere groene threads binnen één OS-thread taken uitvoeren. Goroutines zijn echter een kleinere en efficiëntere vorm van dergelijke groene threads. Deze goroutines verbruiken minder geheugen dan threads en kunnen sneller worden gecreëerd en gewisseld dan threads.

Om een goroutine te gebruiken, volstaat het simpelweg het trefwoord go te gebruiken. Dit maakt het mogelijk om synchrone code intuïtief asynchroon uit te voeren tijdens het ontwikkelproces.

 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}

Deze code converteert op eenvoudige wijze synchrone code, die na 1 seconde "Hello, World!" afdrukt, naar een asynchrone stroom. Hoewel dit voorbeeld eenvoudig is, verbetert de leesbaarheid, zichtbaarheid en begrijpelijkheid van de code aanzienlijk wanneer complexere synchrone code wordt omgezet naar asynchrone code, vergeleken met methoden zoals async/await of promises.

Echter, in veel gevallen kan slechte goroutine-code ontstaan wanneer men de stroom van het eenvoudigweg asynchroon aanroepen van synchrone code en de "fork & join"-stroom (vergelijkbaar met een "verdeel en heers"-stroom) niet begrijpt. In dit artikel zullen wij enkele methoden en technieken introduceren die u kunnen helpen dergelijke situaties te voorkomen.

Gelijktijdigheidsbeheer

context

Het kan verrassend zijn dat context als eerste beheersmechanisme wordt genoemd. Echter, in de Go-taal speelt context een uitmuntende rol in het beheren van de gehele taakstructuur, verdergaand dan enkel een annuleringsfunctie. Voor degenen die er niet bekend mee zijn, zal ik dit pakket kort toelichten.

 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}

De bovenstaande code gebruikt context om na 1 seconde "Context is done!" af te drukken. Met context kan via de Done()-methode worden gecontroleerd of een annulering heeft plaatsgevonden, en via methoden zoals WithCancel, WithTimeout, WithDeadline, en WithValue worden diverse annuleringsmogelijkheden geboden.

Laten we een eenvoudig voorbeeld opstellen. Stel dat u code schrijft om user, post en comment op te halen met behulp van het aggregator-patroon. Indien alle verzoeken binnen 2 seconden moeten worden voltooid, kunt u dit als volgt formuleren:

 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}

Bovenstaande code drukt "Timeout!" af indien niet alle gegevens binnen 2 seconden worden opgehaald, en "All data is fetched!" indien wel alle gegevens zijn opgehaald. Op deze wijze kan context worden gebruikt om annuleringen en time-outs eenvoudig te beheren, zelfs in code waarin meerdere goroutines actief zijn.

Diverse gerelateerde context-functies en -methoden zijn beschikbaar via godoc context. Wij hopen dat u, na bestudering van de basisprincipes, deze gemakkelijk kunt toepassen.

channel

unbuffered channel

Een channel is een hulpmiddel voor communicatie tussen goroutines. Een channel kan worden gecreëerd met make(chan T), waarbij T het type gegevens is dat het channel zal overdragen. Gegevens kunnen via <- worden verzonden en ontvangen via het channel, en het channel kan worden gesloten met close.

 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}

De bovenstaande code drukt 1 en 2 af met behulp van een channel. Deze code toont enkel het verzenden en ontvangen van waarden via een channel. Echter, een channel biedt meer functionaliteit. Laten we eerst ingaan op buffered channel en unbuffered channel. Om te beginnen is het bovenstaande voorbeeld een unbuffered channel, waarbij het verzenden en ontvangen van gegevens via het kanaal gelijktijdig moet plaatsvinden. Indien deze handelingen niet gelijktijdig plaatsvinden, kan er een deadlock optreden.

buffered channel

Wat als de bovenstaande code geen simpele uitvoer is, maar twee processen die zware taken uitvoeren? Als het tweede proces langdurig vastloopt tijdens het lezen en verwerken, zal het eerste proces ook gedurende die tijd pauzeren. Om dergelijke situaties te voorkomen, kunnen we een buffered channel gebruiken.

 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}

De bovenstaande code drukt 1 en 2 af met behulp van een buffered channel. Deze code maakt gebruik van een buffered channel, zodat het verzenden en ontvangen van gegevens via het channel niet gelijktijdig hoeft plaats te vinden. Door een buffer in het kanaal te plaatsen, ontstaat er ruimte voor de opgegeven lengte, waardoor vertragingen in de taken als gevolg van daaropvolgende bewerkingen kunnen worden voorkomen.

select

Bij het omgaan met meerdere kanalen kan de select-instructie eenvoudig een fan-in-structuur implementeren.

 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}

De bovenstaande code creëert drie kanalen die periodiek 1, 2 en 3 verzenden, en gebruikt select om waarden uit de kanalen te ontvangen en af te drukken. Op deze wijze kan select worden gebruikt om gelijktijdig gegevens van meerdere kanalen te ontvangen en deze te verwerken zodra ze beschikbaar komen.

for range

Een channel kan eenvoudig gegevens ontvangen met behulp van for range. Wanneer for range wordt gebruikt op een channel, zal het elke keer dat gegevens aan het channel worden toegevoegd, worden uitgevoerd, en de lus zal eindigen wanneer het channel wordt gesloten.

 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}

De bovenstaande code drukt 1 en 2 af met behulp van een channel. In deze code wordt for range gebruikt om gegevens te ontvangen en af te drukken telkens wanneer deze aan het channel worden toegevoegd. De lus eindigt wanneer het channel wordt gesloten.

Zoals eerder vermeld, kan deze syntaxis ook worden gebruikt voor eenvoudige synchronisatie.

 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}

De bovenstaande code drukt na 1 seconde "Hello, World!" af. In deze code is het channel gebruikt om synchrone code om te zetten naar asynchrone code. Op deze wijze kan een channel worden gebruikt om synchrone code eenvoudig om te zetten naar asynchrone code en om joinpunten te definiëren.

etc

  1. Indien gegevens naar een nil channel worden verzonden of ontvangen, kan een oneindige lus ontstaan, wat leidt tot een deadlock.
  2. Indien gegevens worden verzonden nadat een channel is gesloten, treedt een panic op.
  3. Zelfs indien een channel niet expliciet wordt gesloten, zal de Garbage Collector (GC) het channel sluiten.

mutex

spinlock

Een spinlock is een synchronisatiemethode die herhaaldelijk probeert een lock te verkrijgen door middel van een lus. In de Go-taal kan een spinlock eenvoudig worden geïmplementeerd met behulp van pointers.

 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}

De bovenstaande code implementeert het spinlock-pakket. In deze code is SpinLock geïmplementeerd met behulp van het sync/atomic-pakket. De Lock-methode probeert een lock te verkrijgen met atomic.CompareAndSwapUintptr, en de Unlock-methode geeft de lock vrij met atomic.StoreUintptr. Deze methode probeert de lock ononderbroken te verkrijgen, waardoor de CPU continu wordt gebruikt totdat de lock is verkregen, wat kan leiden tot een oneindige lus. Daarom is het aan te raden spinlock alleen te gebruiken voor eenvoudige synchronisatie of voor korte perioden.

sync.Mutex

Een mutex is een hulpmiddel voor synchronisatie tussen goroutines. De mutex die door het sync-pakket wordt aangeboden, voorziet in methoden zoals Lock, Unlock, RLock en RUnlock. Een mutex kan worden gecreëerd met sync.Mutex, en een lees-/schrijfvergrendeling kan worden gebruikt met sync.RWMutex.

 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}

In de bovenstaande code zullen twee goroutines vrijwel gelijktijdig toegang krijgen tot dezelfde count-variabele. Door mutex te gebruiken om de code die toegang krijgt tot de count-variabele tot een kritieke sectie te maken, kan gelijktijdige toegang tot de count-variabele worden voorkomen. Hierdoor zal deze code altijd 2 afdrukken, ongeacht hoe vaak deze wordt uitgevoerd.

sync.RWMutex

sync.RWMutex is een mutex die onderscheid maakt tussen lees- en schrijfvergrendelingen. Met de methoden RLock en RUnlock kan een leesvergrendeling worden geplaatst en opgeheven.

 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}

De bovenstaande code implementeert ConcurrentMap met behulp van sync.RWMutex. In deze code wordt een leesvergrendeling geplaatst in de Get-methode en een schrijfvergrendeling in de Set-methode, waardoor veilige toegang en wijziging van de data-map mogelijk is. De noodzaak van een leesvergrendeling is dat wanneer er veel leesbewerkingen zijn, meerdere goroutines gelijktijdig leesbewerkingen kunnen uitvoeren door alleen een leesvergrendeling te plaatsen en geen schrijfvergrendeling. Dit verbetert de prestaties in gevallen waarin een schrijfvergrendeling niet nodig is omdat er geen statuswijziging plaatsvindt.

fakelock

fakelock is een eenvoudige truc die sync.Locker implementeert. Deze structuur biedt dezelfde methoden als sync.Mutex, maar voert geen daadwerkelijke bewerkingen uit.

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

De bovenstaande code implementeert het fakelock-pakket. Dit pakket implementeert sync.Locker en biedt de Lock- en Unlock-methoden, maar voert in werkelijkheid geen enkele bewerking uit. De reden waarom dergelijke code nodig is, zal ik bij gelegenheid toelichten.

waitgroup

sync.WaitGroup

sync.WaitGroup is een hulpmiddel om te wachten tot alle goroutine-taken zijn voltooid. Het biedt de methoden Add, Done en Wait. Met de Add-methode wordt het aantal goroutines toegevoegd, met de Done-methode wordt aangegeven dat een goroutine-taak is voltooid, en met de Wait-methode wordt gewacht tot alle goroutine-taken zijn voltooid.

 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}

De bovenstaande code gebruikt sync.WaitGroup om 100 goroutines gelijktijdig de waarde van de variabele c te laten verhogen. In deze code wacht sync.WaitGroup tot alle goroutines zijn voltooid en drukt vervolgens de opgetelde waarde van c af. Hoewel voor het "fork & join" van enkele taken een channel volstaat, is het gebruik van sync.WaitGroup een goede optie voor het "fork & join" van een groot aantal taken.

met slice

In combinatie met slices kan een waitgroup een uitstekend hulpmiddel zijn voor het beheren van gelijktijdige uitvoer zonder locks.

 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}

De bovenstaande code gebruikt uitsluitend een waitgroup om elke goroutine gelijktijdig 10 willekeurige gehele getallen te laten genereren en deze op de toegewezen index op te slaan. In deze code wacht de waitgroup tot alle goroutines zijn voltooid en drukt vervolgens "Done" af. Op deze wijze kan een waitgroup worden gebruikt om meerdere goroutines gelijktijdig taken te laten uitvoeren, gegevens zonder locks op te slaan totdat alle goroutines zijn voltooid, en vervolgens gezamenlijk post-processing uit te voeren.

golang.org/x/sync/errgroup.ErrGroup

errgroup is een uitbreiding van sync.WaitGroup. In tegenstelling tot sync.WaitGroup, annuleert errgroup alle goroutines en retourneert een fout als er een fout optreedt in een van de goroutine-taken.

 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}

De bovenstaande code gebruikt errgroup om 10 goroutines te creëren en een fout te genereren in de 5e goroutine. Ik heb opzettelijk een fout veroorzaakt in de vijfde goroutine om het geval te demonstreren waarin een fout optreedt. In de praktijk kunt u errgroup gebruiken om goroutines te creëren en diverse post-processing uit te voeren wanneer er fouten optreden in individuele goroutines.

once

Een hulpmiddel voor het uitvoeren van code die slechts één keer mag worden uitgevoerd. De gerelateerde code kan worden uitgevoerd via de onderstaande constructor.

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 zorgt ervoor dat de betreffende functie slechts één keer wordt uitgevoerd gedurende het gehele programma.

 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}

De bovenstaande code gebruikt sync.OnceFunc om "Hello, World!" af te drukken. In deze code wordt sync.OnceFunc gebruikt om de once-functie te creëren, en zelfs als de once-functie meerdere keren wordt aangeroepen, wordt "Hello, World!" slechts één keer afgedrukt.

OnceValue

OnceValue zorgt er niet alleen voor dat de betreffende functie slechts één keer wordt uitgevoerd, maar slaat ook de retourwaarde van die functie op en retourneert de opgeslagen waarde bij volgende aanroepen.

 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}

De bovenstaande code gebruikt sync.OnceValue om de variabele c met 1 te verhogen. In deze code wordt sync.OnceValue gebruikt om de once-functie te creëren, en zelfs als de once-functie meerdere keren wordt aangeroepen, wordt de variabele c slechts één keer verhoogd en retourneert de waarde 1.

OnceValues

OnceValues functioneert identiek aan OnceValue, maar kan meerdere waarden retourneren.

 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}

De bovenstaande code gebruikt sync.OnceValues om de variabele c met 1 te verhogen. In deze code wordt sync.OnceValues gebruikt om de once-functie te creëren, en zelfs als de once-functie meerdere keren wordt aangeroepen, wordt de variabele c slechts één keer verhoogd en retourneert de waarde 1.

atomic

Het atomic-pakket biedt atomaire bewerkingen. Het atomic-pakket biedt methoden zoals Add, CompareAndSwap, Load, Store en Swap, maar recentelijk wordt het gebruik van typen zoals Int64, Uint64 en Pointer aanbevolen.

 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}

Dit was een eerder gebruikt voorbeeld. De code verhoogt de variabele c atomair met behulp van het atomic.Int64-type. Met de methoden Add en Load kan de variabele atomair worden verhoogd en gelezen. Bovendien kan de Store-methode worden gebruikt om een waarde op te slaan, de Swap-methode om een waarde te wisselen, en de CompareAndSwap-methode om een waarde te vergelijken en deze te wisselen indien geschikt.

cond

sync.Cond

Het cond-pakket biedt voorwaardelijke variabelen. Het cond-pakket kan worden gecreëerd met sync.Cond en biedt de methoden Wait, Signal en 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}

De bovenstaande code gebruikt sync.Cond om te wachten tot de variabele ready true wordt. In deze code wacht sync.Cond totdat de variabele ready true is en drukt vervolgens "Ready!" af. Op deze wijze kan sync.Cond worden gebruikt om meerdere goroutines tegelijkertijd te laten wachten totdat aan een specifieke voorwaarde is voldaan.

Dit kan worden gebruikt om een eenvoudige queue te implementeren.

 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}

Door sync.Cond op deze wijze te gebruiken, kan efficiënt worden gewacht en opnieuw worden geactiveerd wanneer aan de voorwaarde is voldaan, in plaats van een spin-lock te gebruiken die veel CPU-gebruik vereist.

semaphore

golang.org/x/sync/semaphore.Semaphore

Het semaphore-pakket biedt semaforen. Een semafoor kan worden gecreëerd met golang.org/x/sync/semaphore.Semaphore en biedt de methoden Acquire, Release en 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}

De bovenstaande code demonstreert het gebruik van een semaphore om een semafoor te creëren, te verkrijgen met de Acquire-methode en vrij te geven met de Release-methode. Deze code illustreert hoe een semafoor kan worden verkregen en vrijgegeven met behulp van het semaphore-pakket.

Tot slot

De basisinhoud zou voldoende moeten zijn. Ik hoop dat u, op basis van de inhoud van dit artikel, de methoden voor het beheren van gelijktijdigheid met goroutines begrijpt en deze in de praktijk kunt toepassen. Ik hoop dat dit artikel u van dienst is geweest. Dank u wel.