GoSuda

Go-concurrentiestarterpack

By snowmerak views ...

Overzicht

Korte inleiding

De Go-taal beschikt over veel hulpmiddelen voor het beheer van concurrency. In dit artikel willen we enkele daarvan en een aantal trucs introduceren.

Goroutines?

Een goroutine is een nieuw type concurrency-model dat wordt ondersteund door de Go-taal. Gewoonlijk verkrijgt een programma van het besturingssysteem OS-threads om meerdere taken tegelijkertijd uit te voeren, en voert het taken parallel uit, zoveel als er cores zijn. Om concurrency op een kleinere schaal uit te voeren, worden in de userland groene threads aangemaakt, zodat meerdere groene threads binnen één OS-thread om de beurt taken uitvoeren. Goroutines zijn echter een verdere verkleining en verbetering van dit soort groene threads. Goroutines gebruiken minder geheugen dan threads en kunnen sneller worden aangemaakt en gewisseld dan threads.

Om goroutines te gebruiken, hoeft men alleen het go-sleutelwoord te gebruiken. Hierdoor kan synchrone code tijdens het schrijven van een programma op intuïtieve wijze als asynchrone code worden uitgevoerd.

 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 verandert eenvoudig een synchrone code, die één seconde wacht en vervolgens Hello, World! afdrukt, in een asynchrone stroom. Hoewel het huidige voorbeeld eenvoudig is, worden de leesbaarheid, zichtbaarheid en begrijpelijkheid van de code beter wanneer een iets complexere code van synchroon naar asynchroon wordt omgezet, vergeleken met bestaande methoden zoals async await of promise.

In veel gevallen kunnen echter slechte goroutine-codes ontstaan wanneer men de stroom van het eenvoudig asynchroon aanroepen van synchrone code, en stromen zoals fork & join (die enigszins lijken op divide and conquer), niet begrijpt. Om op deze situaties voorbereid te zijn, zullen we in dit artikel enkele methoden en technieken introduceren.

Concurrency-beheer

context

Het is misschien verrassend dat context als eerste beheertechniek naar voren komt. In de Go-taal speelt context echter, naast een eenvoudige annuleringsfunctie, een uitstekende rol bij het beheer van de volledige taakboom. Voor wie het pakket niet kent, zullen we het 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 één seconde Context is done! af te drukken. Met de methode Done() kan men controleren of een context is geannuleerd, en via methoden zoals WithCancel, WithTimeout, WithDeadline en WithValue worden diverse annuleringsmethoden geboden.

Laten we een eenvoudig voorbeeld maken. Stel dat u een code schrijft die, met behulp van het aggregator-patroon, user, post en comment ophaalt. Stel verder dat alle verzoeken binnen twee seconden moeten zijn voltooid. Dan kunt u dit als volgt schrijven.

 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}

De bovenstaande code drukt Timeout! af als niet alle gegevens binnen twee seconden zijn opgehaald, en All data is fetched! als alle gegevens zijn opgehaald. Op deze manier kan men met context annulering en time-outs eenvoudig beheren, zelfs in code waarin meerdere goroutines draaien.

Diverse context-gerelateerde functies en methoden zijn te vinden op godoc context. We hopen dat u na het bestuderen van de eenvoudige onderdelen deze comfortabel kunt gebruiken.

channel

unbuffered channel

Een channel is een hulpmiddel voor communicatie tussen goroutines. Een channel kan worden aangemaakt met make(chan T). Hierbij is T het type gegevens dat het betreffende channel zal doorgeven. Met <- kunnen gegevens worden verzonden en ontvangen, en met close kan een channel worden 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 gebruikt een channel om 1 en 2 af te drukken. Deze code toont alleen het eenvoudig verzenden en ontvangen van waarden via een channel. Een channel biedt echter meer mogelijkheden. Laten we eerst buffered channel en unbuffered channel bekijken. Vooraf moet worden opgemerkt dat het hierboven getoonde voorbeeld een unbuffered channel is, waarbij het verzenden van gegevens naar het kanaal en het ontvangen van gegevens tegelijkertijd moeten plaatsvinden. Als deze handelingen niet gelijktijdig plaatsvinden, kan een deadlock ontstaan.

buffered channel

Hoe zit het als de bovenstaande code geen eenvoudige afdruk is, maar twee zware processen? Als het tweede proces, terwijl het gegevens leest en verwerkt, lange tijd vastloopt, dan zal het eerste proces gedurende die tijd eveneens stilvallen. Om deze situatie 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 gebruikt een buffered channel om 1 en 2 af te drukken. In deze code is een buffered channel gebruikt, zodat het verzenden van gegevens naar het channel en het ontvangen van gegevens niet tegelijkertijd hoeven plaats te vinden. Door een buffer in het kanaal aan te brengen, ontstaat ruimte ter grootte van die buffer, waardoor vertragingen als gevolg van de invloed van latere taken kunnen worden voorkomen.

select

Wanneer men met meerdere kanalen werkt, kan men met de select-syntaxis 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 maakt drie kanalen die periodiek 1, 2 en 3 doorgeven, en gebruikt select om waarden uit de kanalen te ontvangen en af te drukken. Op deze manier kan men met select tegelijkertijd gegevens van meerdere kanalen ontvangen en elke waarde verwerken zodra die binnenkomt.

for range

Met for range kunnen gegevens van een channel eenvoudig worden ontvangen. Wanneer for range op een kanaal wordt gebruikt, wordt het uitgevoerd telkens wanneer er gegevens aan het kanaal worden toegevoegd, en wordt de lus beëindigd zodra het kanaal is 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 gebruikt een channel om 1 en 2 af te drukken. In deze code gebruikt for range om telkens wanneer gegevens aan het kanaal worden toegevoegd, de gegevens te ontvangen en af te drukken. Wanneer het kanaal wordt gesloten, wordt de lus beëindigd.

Zoals eerder meermaals is getoond, kan deze syntaxis ook als eenvoudig synchronisatiemiddel worden gebruikt.

 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 wacht één seconde en drukt vervolgens Hello, World! af. In deze code is een channel gebruikt om synchrone code om te zetten in asynchrone code. Op deze manier kan men met een channel synchrone code eenvoudig in asynchrone code omzetten en een join-punt instellen.

etc

  1. Wanneer men gegevens naar of van een nil channel verzendt of ontvangt, kan men in een oneindige lus terechtkomen, wat tot een deadlock kan leiden.
  2. Wanneer men gegevens verzendt nadat een kanaal is gesloten, treedt een panic op.
  3. Het is niet noodzakelijk om een kanaal expliciet te sluiten, omdat de GC het kanaal zal opruimen en sluiten.

mutex

spinlock

Een spinlock is een synchronisatiemethode die in een lus herhaaldelijk probeert het slot te verkrijgen. In de Go-taal kan men met behulp van pointers eenvoudig een spinlock implementeren.

 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 pakket spinlock. In deze code is SpinLock geïmplementeerd met het pakket sync/atomic. In de methode Lock wordt geprobeerd het slot te verkrijgen met atomic.CompareAndSwapUintptr, en in de methode Unlock wordt het slot vrijgegeven met atomic.StoreUintptr. Omdat deze methode zonder pauze het slot blijft proberen te verkrijgen, blijft de CPU gedurende het hele wachten op het slot in gebruik, wat tot een oneindige lus kan leiden. Daarom is het verstandig om spinlock alleen te gebruiken voor eenvoudige synchronisatie of voor zeer korte periodes.

sync.Mutex

Een mutex is een hulpmiddel voor synchronisatie tussen goroutines. De mutex die het pakket sync biedt, heeft methoden zoals Lock, Unlock, RLock en RUnlock. Een mutex kan worden aangemaakt met sync.Mutex, en met sync.RWMutex kan men ook een lees-/schrijfslot gebruiken.

 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 hebben twee goroutines vrijwel tegelijkertijd toegang tot dezelfde variabele count. Door de code die toegang tot de variabele count heeft in een kritieke sectie te plaatsen met een mutex, kan gelijktijdige toegang tot count worden voorkomen. Hierdoor zal deze code, ongeacht hoe vaak deze wordt uitgevoerd, altijd 2 afdrukken.

sync.RWMutex

sync.RWMutex is een mutex waarmee men onderscheid kan maken tussen een leeslot en een schrijflot. Met de methoden RLock en RUnlock kan men een leeslot zetten en opheffen.

 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 een ConcurrentMap met sync.RWMutex. In deze code zet de methode Get een leeslot, en de methode Set een schrijflot, zodat de map data veilig kan worden benaderd en gewijzigd. Het leeslot is nodig omdat er, wanneer er veel eenvoudige leesbewerkingen zijn, alleen een leeslot wordt gezet zonder schrijflot, zodat meerdere goroutines tegelijkertijd leesbewerkingen kunnen uitvoeren. Hierdoor kan de prestatie worden verbeterd door alleen een leeslot te zetten in gevallen waarin de staat niet wordt gewijzigd en een schrijflot niet nodig is.

fakelock

fakelock is een eenvoudige truc die sync.Locker implementeert. Deze struct biedt dezelfde methoden als sync.Mutex, maar voert in werkelijkheid geen enkele handeling uit.

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

De bovenstaande code implementeert het pakket fakelock. Dit pakket implementeert sync.Locker en biedt de methoden Lock en Unlock, maar voert in werkelijkheid geen enkele handeling uit. Waarom deze code nodig is, zal ik te zijner tijd toelichten.

waitgroup

sync.WaitGroup

sync.WaitGroup is een hulpmiddel om te wachten tot alle taken van goroutines zijn voltooid. Het biedt de methoden Add, Done en Wait. Met de methode Add voegt men het aantal goroutines toe, met de methode Done meldt men dat de taak van een goroutine is voltooid, en met de methode Wait wacht men tot de taken van alle goroutines 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 zodat 100 goroutines tegelijkertijd een waarde optellen bij de variabele c. In deze code wacht sync.WaitGroup tot alle goroutines zijn voltooid, waarna de bij c opgetelde waarde wordt afgedrukt. Wanneer men slechts enkele taken met fork & join verwerkt, is het gebruik van alleen kanalen voldoende, maar bij het fork & join-verwerken van een grote hoeveelheid taken is sync.WaitGroup ook een goede keuze.

with slice

Wanneer het samen met een slice wordt gebruikt, kan waitgroup een uitstekend hulpmiddel worden voor het beheer van gelijktijdig uitgevoerde taken 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 alleen waitgroup zodat elke goroutine tegelijkertijd een willekeurig geheel getal van 10 cijfers genereert en dit opslaat op de toegewezen index. In deze code wacht waitgroup tot alle goroutines zijn voltooid, waarna Done wordt afgedrukt. Op deze manier kunnen met waitgroup meerdere goroutines tegelijkertijd taken uitvoeren, gegevens zonder locks opslaan totdat alle goroutines zijn afgerond, en daarna gezamenlijk nabewerking uitvoeren.

golang.org/x/sync/errgroup.ErrGroup

errgroup is een pakket dat sync.WaitGroup uitbreidt. In tegenstelling tot sync.WaitGroup annuleert errgroup, wanneer bij een van de taken van de goroutines een fout optreedt, alle goroutines en geeft het de fout terug.

 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 aan te maken, waarbij de vijfde goroutine een fout veroorzaakt. Om te laten zien hoe een fout kan optreden, is opzettelijk in de vijfde goroutine een fout geïntroduceerd. In de praktijk gebruikt men errgroup om goroutines aan te maken en voert men, wanneer er in een goroutine een fout optreedt, diverse nabewerkingen uit.

once

Dit is een hulpmiddel om code uit te voeren die slechts één keer mag worden uitgevoerd. Via de onderstaande constructors kan de betreffende code worden uitgevoerd.

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 er eenvoudigweg voor dat de betreffende functie over de gehele looptijd precies één keer kan worden uitgevoerd.

 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 met sync.OnceFunc de functie once aangemaakt, en ook wanneer once meerdere keren wordt aangeroepen, wordt Hello, World! slechts één keer afgedrukt.

OnceValue

OnceValue zorgt er niet alleen voor dat de betreffende functie over de gehele looptijd precies één keer wordt uitgevoerd, maar slaat ook de retourwaarde van de functie op en geeft bij een volgende aanroep de opgeslagen waarde terug.

 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 met sync.OnceValue de functie once aangemaakt, en ook wanneer once meerdere keren wordt aangeroepen, wordt de variabele c slechts één keer verhoogd en wordt de waarde 1 teruggegeven.

OnceValues

OnceValues werkt op dezelfde manier als OnceValue, maar kan meerdere waarden teruggeven.

 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 met sync.OnceValues de functie once aangemaakt, en ook wanneer once meerdere keren wordt aangeroepen, wordt de variabele c slechts één keer verhoogd en wordt de waarde 1 teruggegeven.

atomic

Het pakket atomic is een pakket dat atomaire bewerkingen biedt. Het pakket atomic biedt methoden zoals Add, CompareAndSwap, Load, Store en Swap, maar tegenwoordig 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 is het voorbeeld dat eerder werd gebruikt. Het gebruikt het type atomic.Int64 om de variabele c atomair te verhogen. Met de methoden Add en Load kan de variabele atomair worden verhoogd en uitgelezen. Daarnaast kan met de methode Store een waarde worden opgeslagen, met de methode Swap een waarde worden vervangen, en met de methode CompareAndSwap een waarde worden vergeleken en, indien passend, worden vervangen.

cond

sync.Cond

Het pakket cond is een pakket dat condition variables biedt. Het pakket cond kan worden aangemaakt 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 de waarde true krijgt. In deze code wacht sync.Cond tot de variabele ready de waarde true krijgt, waarna Ready! wordt afgedrukt. Op deze manier kan men met sync.Cond meerdere goroutines laten wachten totdat aan een bepaalde voorwaarde is voldaan.

Hiermee kan een eenvoudige queue worden geïmplementeerd.

 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 te gebruiken, kan men in plaats van veel CPU-capaciteit te verbruiken met een spin-lock, efficiënt wachten en weer doorgaan zodra aan de voorwaarde is voldaan.

semaphore

golang.org/x/sync/semaphore.Semaphore

Het pakket semaphore is een pakket dat semaforen biedt. Het pakket semaphore kan worden aangemaakt 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 maakt met semaphore een semafoor aan, verkrijgt de semafoor met de methode Acquire en geeft deze weer vrij met de methode Release. In deze code is getoond hoe een semafoor wordt verkregen en vrijgegeven met semaphore.

Tot slot

Het lijkt erop dat de basisinhoud hiermee voldoende is. We hopen dat u op basis van de inhoud van dit artikel begrijpt hoe u goroutines gebruikt om concurrency te beheren, en dat u deze in de praktijk kunt toepassen. We hopen dat dit artikel nuttig voor u is geweest. Dank u wel.