GoSuda

Go Súbežný Štartovací Balík

By snowmerak
views ...

Prehľad

Stručný úvod

Jazyk Go má mnoho nástrojov na správu súbežnosti. V tomto článku si predstavíme niektoré z nich a triky, ktoré s nimi súvisia.

Gorutiny?

Gorutina je nový typ modelu súbežnosti podporovaný jazykom Go. Všeobecne platí, že programy získavajú vlákna OS od operačného systému, aby vykonávali viacero úloh súčasne, pričom tieto úlohy sa vykonávajú paralelne v závislosti od počtu jadier. A na vykonávanie súbežnosti menších jednotiek sa v používateľskom priestore vytvárajú zelené vlákna, takže viacero zelených vlákien môže rotovať a vykonávať úlohy v rámci jedného vlákna OS. V prípade gorutín sú však tieto zelené vlákna ešte menšie a efektívnejšie. Tieto gorutiny spotrebúvajú menej pamäte ako vlákna a dajú sa vytvárať a prepínať rýchlejšie ako vlákna.

Ak chcete používať gorutiny, stačí použiť kľúčové slovo go. To umožňuje intuitívne spúšťať synchrónny kód ako asynchrónny kód v procese písania programu.

 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}

Tento kód jednoducho zmení synchrónny kód, ktorý po 1 sekunde vypíše Hello, World!, na asynchrónny tok. Tento príklad je jednoduchý, ale ak sa trochu zložitejší kód zmení zo synchrónneho na asynchrónny, čitateľnosť, prehľadnosť a zrozumiteľnosť kódu sú lepšie ako pri existujúcich metódach async await alebo promise.

V mnohých prípadoch sa však stáva, že bez pochopenia toku jednoduchého asynchrónneho volania tohto synchrónneho kódu a toku typu fork & join (tok podobný metóde rozdeľuj a panuj) vznikne nevhodný kód gorutín. V tomto článku predstavíme niekoľko metód a techník, ktoré vám pomôžu pripraviť sa na takéto prípady.

Správa súbežnosti

context

Možno je prekvapujúce, že prvou technikou správy je context. V jazyku Go však context nepredstavuje iba funkciu zrušenia, ale zohráva aj vynikajúcu úlohu pri správe celého stromu úloh. Ak s týmto balíkom nie ste oboznámení, stručne ho popíšeme.

 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}

Vyššie uvedený kód používa context na výpis Context is done! po 1 sekunde. context dokáže overiť, či sa má zrušiť prostredníctvom metódy Done() a poskytuje rôzne spôsoby zrušenia prostredníctvom metód WithCancel, WithTimeout, WithDeadline, WithValue atď.

Vytvorme si jednoduchý príklad. Predpokladajme, že píšete kód, ktorý používa vzor aggregator na získanie údajov user, post, comment na načítanie určitých údajov. Ak všetky požiadavky musia byť splnené do 2 sekúnd, môžete to napísať takto:

 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}

Vyššie uvedený kód vypíše Timeout!, ak sa všetky údaje nepodarí načítať do 2 sekúnd, a vypíše All data is fetched!, ak sa všetky údaje načítajú. Použitím context týmto spôsobom môžete jednoducho spravovať zrušenie a časový limit aj v kóde, kde beží viacero gorutín.

Rôzne funkcie a metódy súvisiace s kontextom si môžete pozrieť v godoc context. Dúfame, že sa naučíte používať jednoduché funkcie a metódy, aby ste ich mohli pohodlne používať.

channel

unbuffered channel

channel je nástroj na komunikáciu medzi gorutinami. channel sa dá vytvoriť pomocou make(chan T). V tomto prípade je T typ údajov, ktoré daný channel bude odovzdávať. channel dokáže odosielať a prijímať údaje pomocou <- a channel sa dá zatvoriť pomocou 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}

Vyššie uvedený kód používa channel na výpis čísiel 1 a 2. Tento kód jednoducho ukazuje iba odosielanie a prijímanie hodnôt na channel. channel však poskytuje oveľa viac funkcií. Najprv si povieme niečo o buffered channel a unbuffered channel. Na úvod, vyššie uvedený príklad je unbuffered channel, ktorý vyžaduje, aby odosielanie a prijímanie údajov na kanáli prebiehalo súčasne. Ak sa tieto akcie neuskutočnia súčasne, môže dôjsť k zablokovaniu.

buffered channel

Čo ak vyššie uvedený kód nepredstavuje jednoduchý výpis, ale dva procesy vykonávajúce náročnú úlohu? Ak sa druhý proces pri čítaní a spracovaní dlho zdrží, zastaví sa aj prvý proces. Na zabránenie takejto situácie môžeme použiť buffered channel.

 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}

Vyššie uvedený kód používa buffered channel na výpis čísiel 1 a 2. V tomto kóde sme použili buffered channel, aby sme zabezpečili, že odosielanie a prijímanie údajov na channel nemusia prebiehať súčasne. Pridaním vyrovnávacej pamäte do kanála týmto spôsobom, sa vytvorí rezerva s určitou dĺžkou, ktorá zabráni oneskoreniu úlohy spôsobenému následnou úlohou.

select

Pri práci s viacerými kanálmi môžete jednoducho implementovať štruktúru fan-in pomocou príkazu select.

 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}

Vyššie uvedený kód vytvorí 3 kanály, ktoré pravidelne odosielajú 1, 2 a 3, a používa príkaz select na prijatie hodnôt z kanálov a ich výpis. Použitím príkazu select týmto spôsobom môžete súčasne prijímať údaje z viacerých kanálov a spracovávať ich hneď po prijatí.

for range

channel dokáže jednoducho prijímať údaje pomocou príkazu for range. Ak sa for range používa s kanálom, vykonáva sa vždy, keď sa do kanála pridajú údaje, a ukončí slučku, keď sa kanál zatvorí.

 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}

Vyššie uvedený kód používa channel na výpis čísiel 1 a 2. V tomto kóde používame príkaz for range na prijímanie a výpis údajov vždy, keď sa do kanála pridajú údaje. A keď sa kanál zatvorí, slučku ukončíme.

Ako sme už niekoľkokrát uviedli, túto syntax možno použiť aj pre jednoduché prostriedky synchronizácie.

 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}

Vyššie uvedený kód vypíše Hello, World! po 1 sekunde. V tomto kóde sme pomocou channel zmenili synchrónny kód na asynchrónny kód. Použitím channel týmto spôsobom môžete jednoducho zmeniť synchrónny kód na asynchrónny kód a nastaviť bod join.

etc

  1. Ak odošlete alebo prijmete údaje na kanáli nil, môžete sa dostať do nekonečnej slučky a dôjde k zablokovaniu.
  2. Ak odošlete údaje po zatvorení kanála, dôjde k panike.
  3. Aj keď kanál nie je zatvorený, GC ho uzavrie pri zbere.

mutex

spinlock

spinlock je spôsob synchronizácie, ktorý opakovane prechádza slučkou a pokúša sa získať zámok. V jazyku Go môžeme ľahko implementovať spinlock pomocou ukazovateľov.

 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}

Vyššie uvedený kód je kód, ktorý implementuje balík spinlock. V tomto kóde sme použili balík sync/atomic na implementáciu SpinLock. Metóda Lock sa pokúša získať zámok pomocou atomic.CompareAndSwapUintptr a metóda Unlock uvoľní zámok pomocou atomic.StoreUintptr. Keďže táto metóda sa nepretržite pokúša získať zámok, bude naďalej využívať CPU, kým nezíska zámok, čo môže viesť k nekonečnej slučke. Preto sa spinlock odporúča používať na jednoduchú synchronizáciu alebo na krátky čas.

sync.Mutex

mutex je nástroj na synchronizáciu medzi gorutinami. mutex, ktorý poskytuje balík sync, poskytuje metódy ako Lock, Unlock, RLock, RUnlock. mutex sa dá vytvoriť pomocou sync.Mutex a na použitie zámku na čítanie/zápis sa dá použiť 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}

Vo vyššie uvedenom kóde dve gorutiny pristupujú takmer súčasne k rovnakej premennej count. Ak v tomto prípade použijeme mutex na to, aby sa kód pristupujúci k premennej count stal kritickou sekciou, môžeme zabrániť súbežnému prístupu k premennej count. Potom tento kód bude vždy vypisovať 2 bez ohľadu na to, koľkokrát sa spustí.

sync.RWMutex

sync.RWMutex je mutex, ktorý dokáže rozlišovať medzi zámkami na čítanie a zámkami na zápis. Na uzamknutie a uvoľnenie zámku na čítanie môžete použiť metódy RLock a RUnlock.

 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}

Vyššie uvedený kód je kód, ktorý implementuje ConcurrentMap pomocou sync.RWMutex. V tomto kóde uzamkneme čítanie v metóde Get a uzamkneme zápis v metóde Set, aby sme mohli bezpečne pristupovať a upravovať mapu data. Dôvodom, prečo je potrebný zámok na čítanie, je to, že v prípade mnohých jednoduchých operácií čítania je možné vykonať viacero operácií čítania súčasne bez uzamknutia zápisu, ale len s uzamknutím čítania. Týmto spôsobom môžeme zlepšiť výkon tým, že uzamkneme iba čítanie, ak nedochádza k žiadnej zmene stavu a nie je potrebné uzamknúť zápis.

fakelock

fakelock je jednoduchý trik, ktorý implementuje sync.Locker. Táto štruktúra poskytuje rovnaké metódy ako sync.Mutex, ale v skutočnosti nerobí nič.

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

Vyššie uvedený kód je kód, ktorý implementuje balík fakelock. Tento balík implementuje sync.Locker a poskytuje metódy Lock a Unlock, ale v skutočnosti nerobí nič. Prečo je takýto kód potrebný, popíšeme, ak sa naskytne príležitosť.

waitgroup

sync.WaitGroup

sync.WaitGroup je nástroj, ktorý čaká, kým sa nedokončia všetky úlohy gorutín. Poskytuje metódy Add, Done a Wait. Metóda Add pridáva počet gorutín, metóda Done signalizuje, že úloha gorutiny je dokončená. A metóda Wait čaká, kým sa nedokončia všetky úlohy gorutín.

 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}

Vyššie uvedený kód používa sync.WaitGroup na súčasné pridávanie hodnoty k premennej c pomocou 100 gorutín. V tomto kóde sa používa sync.WaitGroup na čakanie, kým všetky gorutiny neskončia, a potom sa vytlačí hodnota, ktorá bola pridaná k premennej c. Aj keď na jednoduché fork & join operácie stačí použiť iba kanály, použitie sync.WaitGroup je dobrou voľbou pre fork & join veľkého množstva operácií.

so slice

Ak sa používa so slicom, waitgroup môže byť vynikajúcim nástrojom na riadenie súbežných operácií bez zámkov.

 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}

Vyššie uvedený kód používa iba waitgroup na súčasné generovanie 10 náhodných celých čísel každou gorutinou a ich uloženie do priradeného indexu. V tomto kóde sa používa waitgroup na čakanie, kým všetky gorutiny neskončia, a potom sa vytlačí Done. Použitím waitgroup týmto spôsobom môže viacero gorutín vykonávať operácie súčasne, ukladať dáta bez zámkov, kým všetky gorutiny neskončia, a hromadne spracovávať následné operácie po skončení operácií.

golang.org/x/sync/errgroup.ErrGroup

errgroup je balík, ktorý rozširuje sync.WaitGroup. Na rozdiel od sync.WaitGroup, ak sa počas operácie gorutiny vyskytne chyba, errgroup zruší všetky gorutiny a vráti chybu.

 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}

Vyššie uvedený kód používa errgroup na vytvorenie 10 gorutín a vyvolanie chyby v 5. gorutine. Zámerne sme vyvolali chybu v piatej gorutine, aby sme ukázali prípad, keď sa vyskytne chyba. V skutočnosti by ste však mali používať errgroup na vytváranie gorutín a vykonávať rôzne následné operácie v prípade, že sa v každej gorutine vyskytne chyba.

once

Nástroj na vykonanie kódu, ktorý sa má vykonať iba raz. Súvisiaci kód je možné vykonať prostredníctvom nižšie uvedeného konštruktora.

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 jednoducho umožňuje, aby sa daná funkcia vykonala iba raz v celom rozsahu.

 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}

Vyššie uvedený kód používa sync.OnceFunc na vytlačenie Hello, World!. V tomto kóde sa pomocou sync.OnceFunc vytvorí funkcia once a aj keď sa funkcia once zavolá viackrát, Hello, World! sa vytlačí iba raz.

OnceValue

OnceValue nezabezpečuje len to, že sa daná funkcia vykoná iba raz v celom rozsahu, ale aj ukladá návratovú hodnotu danej funkcie a pri opätovnom zavolaní vráti uloženú hodnotu.

 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}

Vyššie uvedený kód používa sync.OnceValue na inkrementovanie premennej c o 1. V tomto kóde sa pomocou sync.OnceValue vytvorí funkcia once a aj keď sa funkcia once zavolá viackrát, vráti sa 1, pretože premenná c sa inkrementuje iba raz.

OnceValues

OnceValues funguje rovnako ako OnceValue, ale môže vrátiť viacero hodnôt.

 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}

Vyššie uvedený kód používa sync.OnceValues na inkrementovanie premennej c o 1. V tomto kóde sa pomocou sync.OnceValues vytvorí funkcia once a aj keď sa funkcia once zavolá viackrát, vráti sa 1, pretože premenná c sa inkrementuje iba raz.

atomic

Balík atomic je balík, ktorý poskytuje atómové operácie. Balík atomic poskytuje metódy ako Add, CompareAndSwap, Load, Store, Swap, ale v poslednej dobe sa odporúča používať typy ako Int64, Uint64, 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}

Toto je príklad, ktorý bol použitý predtým. Používa sa typ atomic.Int64 na atómové inkrementovanie premennej c. Pre inkrementovanie premennej a jej načítanie je možné použiť metódy Add a Load. Okrem toho je možné uložiť hodnotu pomocou metódy Store, vymeniť hodnotu pomocou metódy Swap a porovnať hodnotu a potom ju vymeniť, ak je vhodná, pomocou metódy CompareAndSwap.

cond

sync.Cond

Balík cond je balík, ktorý poskytuje podmienkové premenné. Balík cond je možné vytvoriť pomocou sync.Cond a poskytuje metódy 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("Ready!")
25}

Vyššie uvedený kód používa sync.Cond na čakanie, kým sa premenná ready nestane true. V tomto kóde sa pomocou sync.Cond čaká, kým sa premenná ready nestane true, a potom sa vytlačí Ready!. Použitím sync.Cond týmto spôsobom je možné zabezpečiť, aby viacero gorutín čakalo, kým sa splní určitá podmienka.

Pomocou toho je možné implementovať jednoduchý queue.

 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}

Použitím sync.Cond týmto spôsobom je možné efektívne čakať a opäť fungovať, keď je splnená podmienka, namiesto používania veľkého množstva CPU s spin-lock.

semaphore

golang.org/x/sync/semaphore.Semaphore

Balík semaphore je balík, ktorý poskytuje semafory. Balík semaphore je možné vytvoriť pomocou golang.org/x/sync/semaphore.Semaphore a poskytuje metódy 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("Acquired!")
14    } else {
15        fmt.Println("Not Acquired!")
16    }
17
18    s.Release(1)
19}

Vyššie uvedený kód používa semaphore na vytvorenie semaforu a semafor sa používa na získanie semaforu pomocou metódy Acquire a uvoľnenie semaforu pomocou metódy Release. V tomto kóde sme ukázali, ako pomocou semaphore získať a uvoľniť semafor.

Záver

Zdá sa, že základné veci sú tu pokryté. Dúfam, že na základe obsahu tohto článku chápete, ako používať gorutiny na riadenie súbežnosti, a budete ich môcť skutočne používať. Dúfam, že vám tento článok pomohol. Ďakujem.