GoSuda

Go egyidejűségi kezdőcsomag

By snowmerak
views ...

Áttekintés

Rövid bevezető

A Go nyelv számos eszközt biztosít a konkurens folyamatok kezeléséhez. Ebben a cikkben ezek közül néhányat és trükköket mutatunk be.

Goroutine?

A goroutine egy újfajta konkurens modell, amelyet a Go nyelv támogat. Általában a programok OS szálakat kapnak az operációs rendszertől, hogy egyszerre több feladatot hajtsanak végre, és a magok számának megfelelően párhuzamosan végezzék el a feladatokat. A kisebb egységű konkurens feladatok elvégzéséhez pedig a felhasználói térben zöld szálakat hoznak létre, amelyek egy OS szálon belül többször is futnak. A goroutine-ok azonban még kisebbé és hatékonyabbá tették ezt a fajta zöld szálat. Ezek a goroutine-ok kevesebb memóriát használnak, mint a szálak, és gyorsabban hozhatók létre és cserélhetők ki, mint a szálak.

A goroutine használatához egyszerűen a go kulcsszót kell használni. Ez lehetővé teszi, hogy a program írásakor intuitív módon szinkron kódot aszinkron kódként futtassunk.

 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}

Ez a kód egyszerűen egy szinkron kódot, amely 1 másodperc várakozás után kiírja a Hello, World! szöveget, aszinkron folyamattá alakít. A jelenlegi példa egyszerű, de ha egy kicsit bonyolultabb kódot alakítunk át szinkronról aszinkronra, a kód olvashatósága, átláthatósága és megértése jobb lesz, mint a hagyományos async await vagy promise módszereknél.

Sok esetben azonban, ha nem értjük meg ezt az egyszerű aszinkron hívási folyamatot, és a fork & join típusú folyamatokat (amelyek a divide and conquer elvhez hasonlóak), rossz goroutine kódok jöhetnek létre. Ebben a cikkben bemutatunk néhány módszert és technikát, amelyek segíthetnek az ilyen esetekben.

Konkurencia kezelés

context

Lehet, hogy meglepő, hogy az első kezelési technika a context használata. A Go nyelvben azonban a context nem csupán egy egyszerű megszakítási funkció, hanem kiváló szerepet játszik a teljes feladatfa kezelésében is. Azok számára, akik nem ismerik, röviden ismertetem ezt a csomagot.

 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}

A fenti kód a context használatával 1 másodperc múlva kiírja a Context is done! szöveget. A context a Done() metódusával ellenőrizheti, hogy megszakították-e, és különféle megszakítási módszereket biztosít a WithCancel, WithTimeout, WithDeadline, WithValue metódusokkal.

Nézzünk egy egyszerű példát. Tegyük fel, hogy valamilyen adat lekéréséhez egy aggregator mintát használunk, amellyel lekérjük a user, post és comment adatokat. És ha minden kérésnek 2 másodpercen belül kell megérkeznie, akkor az alábbiak szerint írhatjuk meg a kódot.

 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}

A fenti kód kiírja a Timeout! üzenetet, ha 2 másodpercen belül nem sikerül lekérni az összes adatot, és kiírja az All data is fetched! üzenetet, ha sikerül lekérni az összes adatot. A context ilyen módon történő használatával könnyen kezelhetjük a megszakításokat és az időkorlátokat még több goroutine-t használó kódokban is.

A context-hez kapcsolódó különféle függvények és metódusok a godoc context oldalon találhatók. Remélem, hogy miután elsajátított néhány egyszerűbb dolgot, kényelmesen használhatja majd.

channel

unbuffered channel

A channel egy eszköz a goroutine-ok közötti kommunikációhoz. A channel a make(chan T) paranccsal hozható létre. Ebben az esetben a T az a adattípus, amelyet az adott channel továbbít. A channel a <- segítségével adatokat küldhet és fogadhat, és a close segítségével lezárható.

 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}

A fenti kód a channel segítségével kiírja az 1-et és a 2-t. Ez a kód egyszerűen csak azt mutatja be, hogyan küldhetünk és fogadhatunk értékeket a channel-en keresztül. A channel azonban ennél többet is kínál. Először is, nézzük meg a buffered channel és az unbuffered channel közötti különbséget. Kezdésként a fenti példa egy unbuffered channel, ahol az adatok csatornába küldésének és az adatok fogadásának egyszerre kell megtörténnie. Ha ezek az események nem történnek meg egyszerre, holtpont alakulhat ki.

buffered channel

Mi van akkor, ha a fenti kód nem egyszerűen csak kiír, hanem két nehéz feladatot végző folyamat? Ha a második folyamat olvasás közben hosszú ideig akadozik, akkor az első folyamat is megáll erre az időre. Ennek elkerülése érdekében használhatunk buffered channel-t.

 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}

A fenti kód egy buffered channel segítségével írja ki az 1-et és a 2-t. Ebben a kódban buffered channel-t használtunk, hogy az adatok csatornába küldésének és az adatok fogadásának nem kell egyszerre megtörténnie. Ha puffer van a csatornán, akkor ez a puffer méretének megfelelő mozgásteret biztosít, amivel megelőzhető a hátrébb lévő feladatokból adódó késleltetés.

select

Több csatorna kezelésekor a select utasítással könnyen megvalósíthatunk egy fan-in struktúrát.

 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}

A fenti kód 3 csatornát hoz létre, amelyek rendszeresen 1-et, 2-t és 3-at küldenek, és a select segítségével fogadja és írja ki az értékeket a csatornákból. A select ilyen módon történő használatával egyidejűleg fogadhatunk adatokat több csatornából, és feldolgozhatjuk az értékeket, amint azok a csatornákból megérkeznek.

for range

A channel könnyen fogadhat adatokat a for range használatával. Ha egy csatornát használunk for range ciklusban, akkor a ciklus minden alkalommal lefut, amikor új adat kerül a csatornába, és a ciklus leáll, amikor a csatorna bezárul.

 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}

A fenti kód a channel segítségével kiírja az 1-et és a 2-t. Ez a kód a for range segítségével fogadja az adatokat, és minden alkalommal kiírja őket, amikor új adat kerül a csatornába. A ciklus akkor áll le, amikor a csatorna bezárul.

Ahogy fentebb már többször írtuk, ez a szintaxis egyszerű szinkronizálásra is használható.

 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}

A fenti kód 1 másodperc várakozás után kiírja a Hello, World! szöveget. Ebben a kódban a channel segítségével szinkron kódot aszinkron kóddá alakítottunk. A channel ilyen módon történő használatával könnyen átalakíthatjuk a szinkron kódot aszinkron kóddá, és beállíthatjuk a join pontokat.

etc

  1. Ha adatokat küldünk vagy fogadunk egy nil csatornán, végtelen ciklusba kerülhetünk, ami holtpontot okozhat.
  2. Ha adatokat küldünk egy lezárt csatornába, pánik keletkezik.
  3. Nem kell bezárni a csatornákat, mert a GC bezárja azokat, amikor összegyűjti őket.

mutex

spinlock

A spinlock egy szinkronizációs módszer, amely ciklusban próbálja meg folyamatosan megszerezni a zárat. A Go nyelvben a mutatók segítségével egyszerűen megvalósíthatunk egy spinlock-ot.

 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}

A fenti kód a spinlock csomagot valósítja meg. Ebben a kódban a sync/atomic csomag segítségével valósítottuk meg a SpinLock zárat. A Lock metódus az atomic.CompareAndSwapUintptr segítségével próbálja meg megszerezni a zárat, az Unlock metódus pedig az atomic.StoreUintptr segítségével oldja fel a zárat. Ez a módszer folyamatosan próbálja megszerezni a zárat, ezért addig használja a CPU-t, amíg meg nem szerzi a zárat, ami végtelen ciklusba kerülhet. Ezért a spinlock-ot egyszerű szinkronizálásra vagy csak rövid ideig tartó használatra érdemes használni.

sync.Mutex

A mutex egy eszköz a goroutine-ok közötti szinkronizáláshoz. A sync csomag által biztosított mutex olyan metódusokat kínál, mint a Lock, Unlock, RLock és RUnlock. A mutex létrehozható a sync.Mutex paranccsal, és a sync.RWMutex segítségével olvasási/írási zárakat is használhatunk.

 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}

A fenti kódban két goroutine szinte egyidejűleg fér hozzá ugyanahhoz a count változóhoz. Ha itt a mutex segítségével a count változóhoz hozzáférő kódot kritikus szakasszá tesszük, akkor megakadályozhatjuk a count változó egyidejű elérését. Ezzel a kóddal mindig 2 lesz a kimenet.

sync.RWMutex

A sync.RWMutex egy olyan mutex, amely elkülönítve tudja használni az olvasási és írási zárakat. Az RLock és az RUnlock metódusok segítségével olvasási zárakat állíthatunk be és oldhatunk fel.

 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}

A fenti kód a sync.RWMutex segítségével valósítja meg a ConcurrentMap-ot. Ebben a kódban a Get metódusban olvasási zárat állítunk be, a Set metódusban pedig írási zárat, így biztonságosan hozzáférhetünk és módosíthatjuk a data map-et. Az olvasási zárra azért van szükség, mert ha sok az egyszerű olvasási művelet, akkor az írási zár használata helyett csak olvasási zárat állíthatunk be, így több goroutine is végezhet olvasási műveleteket egyidejűleg. Ezáltal növelhetjük a teljesítményt, ha nincs szükség írási zárra az állapot változása hiányában.

fakelock

A fakelock egy egyszerű trükk a sync.Locker megvalósítására. Ez a struktúra ugyanazokat a metódusokat kínálja, mint a sync.Mutex, de valójában nem csinál semmit.

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

A fenti kód a fakelock csomagot valósítja meg. Ez a csomag a sync.Locker megvalósításával Lock és Unlock metódusokat biztosít, de valójában nem csinál semmit. Hogy miért van szükség egy ilyen kódra, azt majd egy későbbi alkalommal kifejtem.

waitgroup

sync.WaitGroup

A sync.WaitGroup egy eszköz, amellyel megvárhatjuk, amíg az összes goroutine befejezi a feladatát. Az Add, Done és Wait metódusokat kínálja, az Add metódussal adjuk hozzá a goroutine-ok számát, a Done metódussal értesítjük a csoportot, hogy egy goroutine befejezte a feladatát, és a Wait metódussal várunk, amíg az összes goroutine befejezi a feladatát.

 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}

A fenti kód a sync.WaitGroup használatával 100 gorutint indít, amelyek egyidejűleg növelik a c változó értékét. Ez a kód a sync.WaitGroup segítségével megvárja, amíg az összes gorutin befejeződik, majd kiírja a c változóhoz hozzáadott értéket. Egyszerű esetekben, ahol néhány feladatot kell fork & join módon végrehajtani, csatornák használata is elegendő lehet, azonban nagyszámú feladat fork & join műveletéhez a sync.WaitGroup használata is jó megoldás.

slice-szal

Szeletekkel együtt használva a waitgroup kiváló eszköz lehet a párhuzamos feladatok kezelésére, zárak nélkül.

 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}

A fenti kód kizárólag a waitgroup-ot használja arra, hogy 10 gorutin egyidejűleg generáljon 10 véletlen egész számot, és azokat a hozzájuk rendelt indexeken tárolja. Ez a kód a waitgroup segítségével megvárja az összes gorutin befejeződését, majd kiírja a Done üzenetet. A waitgroup ilyen módon történő használatával több gorutin egyidejűleg végezhet el feladatokat, zárak nélkül tárolhat adatokat, és a feladatok befejezése után együttesen végezhető el az utófeldolgozás.

golang.org/x/sync/errgroup.ErrGroup

Az errgroup a sync.WaitGroup kiterjesztése. Az errgroup a sync.WaitGroup-től eltérően, ha a gorutinok egyikében hiba lép fel, akkor az összes gorutint megszakítja és hibát ad vissza.

 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}

A fenti kód az errgroup segítségével 10 gorutint hoz létre, és az 5. gorutinban hibát generál. Szándékosan a 5. gorutinban generálunk hibát, hogy bemutassuk a hibás eset kezelését. A valóságban azonban az errgroup használatával gorutinokat létrehozva, a hibák előfordulása esetén különféle utóműveleteket lehet végrehajtani.

once

Ez az eszköz olyan kód végrehajtására szolgál, amely csak egyszer futhat le. Az alábbi konstruktorokkal lehet futtatni a kódot.

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

A OnceFunc egyszerűen biztosítja, hogy a megadott függvény csak egyszer futhasson le a teljes futás során.

 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}

A fenti kód a sync.OnceFunc segítségével a Hello, World! üzenetet írja ki. Ebben a kódban a sync.OnceFunc segítségével hozunk létre egy once függvényt, és bár a once függvényt többször is meghívjuk, a Hello, World! csak egyszer jelenik meg.

OnceValue

A OnceValue nemcsak azt biztosítja, hogy a megadott függvény csak egyszer fusson le, hanem a függvény visszatérési értékét is eltárolja, így a későbbi hívásokkor a már eltárolt értéket adja vissza.

 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}

A fenti kód a sync.OnceValue használatával a c változót növeli 1-gyel. Ebben a kódban a sync.OnceValue segítségével egy once függvényt hozunk létre, és bár az once függvényt többször is meghívjuk, a c változó csak egyszer növekszik 1-gyel, és minden alkalommal 1-et ad vissza.

OnceValues

A OnceValues ugyanúgy működik, mint a OnceValue, de több értéket is vissza tud adni.

 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}

A fenti kód a sync.OnceValues használatával a c változót növeli 1-gyel. Ebben a kódban a sync.OnceValues segítségével egy once függvényt hozunk létre, és bár az once függvényt többször is meghívjuk, a c változó csak egyszer növekszik 1-gyel, és minden alkalommal 1-et ad vissza.

atomic

Az atomic csomag atomi műveleteket biztosít. Az atomic csomag olyan metódusokat biztosít, mint az Add, CompareAndSwap, Load, Store, Swap, de a közelmúltban az olyan típusok használata ajánlott, mint az 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}

Ez az előző példában is használt kód. Az atomic.Int64 típust használva a c változó értékét atomi módon növeljük. Az Add metódussal és a Load metódussal atomi módon tudjuk növelni a változót és kiolvasni az értékét. Emellett a Store metódussal értéket menthetünk, a Swap metódussal értéket cserélhetünk, a CompareAndSwap metódussal pedig összehasonlítás után, ha szükséges, cserélhetünk értéket.

cond

sync.Cond

A cond csomag feltételváltozókat biztosít. A cond csomag a sync.Cond segítségével hozható létre, és olyan metódusokat biztosít, mint a 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}

A fenti kód a sync.Cond használatával megvárja, amíg a ready változó true lesz. Ez a kód a sync.Cond segítségével megvárja, hogy a ready változó true legyen, majd kiírja a Ready! üzenetet. A sync.Cond ilyen módon történő használatával több gorutin egyidejűleg tudja megvárni egy adott feltétel teljesülését.

Ezt felhasználva egy egyszerű queue-t is implementálhatunk.

 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}

A sync.Cond ilyen használatával ahelyett, hogy spin-lock használatával sok CPU-erőforrást használnánk, hatékonyan várakozhatunk, és a feltétel teljesülésekor folytathatjuk a működést.

semaphore

golang.org/x/sync/semaphore.Semaphore

A semaphore csomag szemafort biztosít. A semaphore csomag a golang.org/x/sync/semaphore.Semaphore segítségével hozható létre, és olyan metódusokat biztosít, mint az 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}

A fenti kód szemafort hoz létre a semaphore használatával, majd a szemafort az Acquire metódussal szerzi meg, és a Release metódussal szabadítja fel. Ez a kód bemutatja a szemafort megszerzésének és felszabadításának módját a semaphore használatával.

Befejezés

Úgy tűnik, hogy az alapvető dolgok eddig elegendőek. Remélem, hogy ezen cikk tartalma alapján megértetted a párhuzamosság kezelésének módját gorutinok használatával, és ezt a gyakorlatban is tudod használni. Remélem, hogy ez a cikk hasznos volt számodra. Köszönöm.