GoSuda

Go párhuzamossági kezdőcsomag

By snowmerak views ...

Áttekintés

Rövid bevezetés

A Go nyelvben számos eszköz áll rendelkezésre a párhuzamosság kezelésére. Ebben a cikkben ezek közül néhányat és néhány fogást szeretnék bemutatni.

Goroutine?

A goroutine a Go nyelv által támogatott, új típusú párhuzamossági modell. Általában a program az egyidejű több feladat végrehajtásához OS-szálakat kap az operációs rendszertől, és ezeket a processzormagok számával megegyező mértékben párhuzamosan futtatja. Kisebb egységű párhuzamosság eléréséhez pedig felhasználói térben zöld szálakat hozunk létre, így egyetlen OS-szálon belül több zöld szál váltakozva hajtja végre a feladatokat. A goroutine-ok esetében azonban ezt a zöld szál jellegű megoldást még kisebbé és hatékonyabbá tették. Ezek a goroutine-ok kevesebb memóriát használnak, mint a szálak, és gyorsabban hozhatók létre és cserélhetők le, mint azok.

A goroutine használatához egyszerűen csak a go kulcsszót kell alkalmazni. Ez a program írása során intuitív módon teszi lehetővé, hogy a szinkron kódot aszinkron módon futtassuk.

 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 egy 1 másodpercig tartó szünet után kiírja a Hello, World! üzenetet, és a szinkron kódot aszinkron folyamattá alakítja. A jelenlegi példa egyszerű, de ha valamivel bonyolultabb kódot alakítunk át szinkron kódból aszinkron kóddá, a kód olvashatósága, átláthatósága és érthetősége jobb lesz, mint a hagyományos async await vagy promise jellegű megoldásokkal.

Sok esetben azonban, ha nem értjük meg az ilyen szinkron kódok egyszerű aszinkron hívásának folyamatát, valamint a fork & join jellegű folyamatot (amely hasonló a divide and conquer, azaz oszd meg és uralkodj módszerhez), rossz goroutine-kód születhet. Ezekre az esetekre készülve ebben a cikkben bemutatok néhány módszert és technikát.

Párhuzamosság kezelése

context

Talán meglepő, hogy az első kezelési technika a context. A Go nyelvben azonban a context a puszta megszakítási funkciónál többet nyújt, és kiválóan alkalmas a teljes feladatfa kezelésére. Ha valakinek nem ismerős a csomag, röviden bemutatom.

 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 segítségével 1 másodperc elteltével kiírja a Context is done! üzenetet. A context a Done() metóduson keresztül ellenőrizhető, és a WithCancel, WithTimeout, WithDeadline, WithValue stb. metódusokon keresztül különböző megszakítási módokat biztosít.

Készítsünk egy egyszerű példát. Tegyük fel, hogy egy aggregator mintát alkalmazva írunk kódot, amely a user, post és comment adatokat kéri le, és az összes kérésnek 2 másodpercen belül kell megtörténnie. Ekkor a következőképpen írhatjuk meg:

 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, ha 2 másodpercen belül nem sikerül lekérni az összes adatot, kiírja a Timeout! üzenetet, ha pedig minden adat megérkezik, a All data is fetched! üzenetet. Ilyen módon a context használatával több goroutine-t futtató kódban is egyszerűen kezelhetők a megszakítások és az időtúllépések.

A context-hez kapcsolódó különféle függvények és metódusok a godoc context oldalon tekinthetők meg. Remélem, hogy az egyszerűbb részeket megtanulva kényelmesen tudják majd használni.

channel

unbuffered channel

A channel a goroutine-ok közötti kommunikáció eszköze. A channel a make(chan T) segítségével hozható létre. Itt a T az adott channel által továbbított adat típusa. A channelen a <- operátorral lehet adatot küldeni és fogadni, a channel pedig a close függvénnyel zárható le.

 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. Ebben a kódban csupán egyszerű küldést és fogadást mutattam be. A channel ennél több funkciót is kínál. Először a buffered channel és az unbuffered channel fogalmát ismertetem. Mielőtt továbbmennénk, megjegyzem, hogy a fenti példa unbuffered channel, amelyben az adat küldésének és fogadásának egyidejűleg kell megtörténnie. Ha ezek a műveletek nem egyszerre történnek meg, deadlock léphet fel.

buffered channel

Mi a helyzet akkor, ha a fenti kód nem egyszerű kiírást végez, hanem két nehéz feladatot végző folyamatról van szó? Ha a második folyamat a beolvasás után hosszú ideig tart a feldolgozással, akkor az első folyamat is megáll erre az időre. Az ilyen helyzet elkerülésére használhatunk buffered channelt.

 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 a buffered channel segítségével kiírja az 1-et és a 2-t. Ebben a kódban a buffered channel használatával elértük, hogy a channelbe történő küldés és a fogadás nem kell, hogy egyidejűleg történjen meg. Ha a csatornán puffer van, akkor a pufferhossznak megfelelő tartalék keletkezik, így megelőzhető a későbbi feladatok hatására fellépő munkakésedelem.

select

Több csatorna kezelésekor a select szintaxis használatával könnyen megvalósítható a fan-in szerkezet.

 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 három csatornát hoz létre, amelyek rendszeres időközönként az 1, 2 és 3 értékeket továbbítják, a select segítségével pedig a csatornákról érkező értékeket kiírja. Ilyen módon a select használatával egyszerre több csatornán érkező adatokat fogadhatunk, és az értékeket abban a sorrendben dolgozhatjuk fel, ahogyan megérkeznek.

for range

A channel a for range segítségével egyszerűen fogadhat adatokat. Ha a for rangeet csatornára alkalmazzuk, akkor minden alkalommal lefut, amikor a csatornára új adat kerül, a ciklus pedig a csatorna lezárásakor ér véget.

 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. Ebben a kódban a for range használatával minden alkalommal fogadjuk és kiírjuk az adatot, amikor új adat kerül a csatornára. A csatorna lezárásakor pedig a ciklus véget ér.

Ahogy korábban többször leírtam, ez a szintaxis egyszerű szinkronizációs eszközként 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ásodpercnyi szünet után kiírja a Hello, World! üzenetet. Ebben a kódban a channel segítségével a szinkron kódot aszinkron kóddá alakítottuk. Ilyen módon a channel használatával könnyen átalakíthatjuk a szinkron kódot aszinkron kóddá, és kijelölhetjük a join pontot.

etc

  1. Ha nil channelbe küldünk vagy abból fogadunk adatot, végtelen ciklusba kerülhetünk, ami deadlockot okozhat.
  2. Ha a csatornát lezárás után küldünk bele adatot, panic lép fel.
  3. Nem feltétlenül kell lezárni a csatornát, mivel a GC összegyűjtéskor lezárja azt.

mutex

spinlock

A spinlock egy olyan szinkronizációs módszer, amely ciklusban folyamatosan megpróbálja megszerezni a zárat. A Go nyelvben mutatók segítségével egyszerűen implementálhatunk spinlockot.

 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 csomag megvalósítása. Ebben a kódban a sync/atomic csomag segítségével implementáltuk a SpinLockot. A Lock metódusban az atomic.CompareAndSwapUintptr használatával próbáljuk megszerezni a zárat, az Unlock metódusban pedig az atomic.StoreUintptr segítségével szabadítjuk fel. Ez a módszer megállás nélkül próbálkozik a zár megszerzésével, ezért a zár megszerzéséig folyamatosan terheli a CPU-t, és végtelen ciklusba kerülhet. Ezért a spinlockot célszerű egyszerű szinkronizációra vagy csak rövid ideig tartó használatra alkalmazni.

sync.Mutex

A mutex a goroutine-ok közötti szinkronizáció eszköze. A sync csomag által biztosított mutex a Lock, Unlock, RLock, RUnlock stb. metódusokat kínálja. A mutex sync.Mutex típussal hozható létre, a sync.RWMutex típussal pedig olvasási/írási zár is használható.

 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 szinte egyidejűleg két goroutine fér hozzá ugyanahhoz a count változóhoz. Ha a count változóhoz történő hozzáférést tartalmazó kódot mutex segítségével kritikus szakaszként jelöljük, megakadályozhatjuk a count változó egyidejű elérését. Ekkor a kód bárhányszor futtatva mindig 2-t fog kiírni.

sync.RWMutex

A sync.RWMutex olyan mutex, amely az olvasási és az írási zárakat megkülönböztetve használható. Az RLock és RUnlock metódusokkal olvasási zárat helyezhetünk el é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 ConcurrentMapot. Ebben a kódban a Get metódus olvasási zárat, a Set metódus írási zárat alkalmaz, így a data map biztonságosan érhető el és módosítható. Az olvasási zárra azért van szükség, mert ha sok az egyszerű olvasási művelet, akkor íráscsak olvasási zárat alkalmazva több goroutine is egyidejűleg végezhet olvasást. Így azokban az esetekben, amikor az állapot megváltoztatása nem szükséges, és nem kell írási zárat alkalmazni, csupán olvasási zárral javítható a teljesítmény.

fakelock

A fakelock a sync.Locker interfészt megvalósító egyszerű trükk. Ez a struktúra ugyanazokat a metódusokat biztosítja, mint a sync.Mutex, de valójában nem végez semmilyen műveletet.

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

A fenti kód a fakelock csomag megvalósítása. Ez a csomag implementálja a sync.Locker interfészt, és biztosítja a Lock és Unlock metódusokat, de valójában egyiknek sincs műveleti hatása. Azt, hogy miért van szükség ilyen kódra, alkalmi jelleggel később fogom leírni.

waitgroup

sync.WaitGroup

A sync.WaitGroup olyan eszköz, amely megvárja, amíg a goroutine-ok feladatai mind befejeződnek. Az Add, Done, Wait metódusokat biztosítja: az Add metódussal növelhetjük a goroutine-ok számát, a Done metódussal jelezzük, hogy egy goroutine feladata befejeződött. A Wait metódussal pedig megvárjuk, amíg az összes goroutine feladata befejeződik.

 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 segítségével 100 goroutine-t indít, amelyek egyidejűleg adnak hozzá értéket a c változóhoz. Ebben a kódban a sync.WaitGroup segítségével megvárjuk, amíg az összes goroutine befejeződik, majd kiírjuk a c változóba összegyűjtött értéket. Néhány feladat fork & join jellegű kezelésére egyszerűen elegendő csak a csatornákat használni, nagyszámú feladat fork & join kezelésénél azonban a sync.WaitGroup használata is jó választás.

with slice

Ha szelettel (slice) együtt használjuk, a waitgroup zár nélkül is kiváló eszköze lehet a párhuzamosan futó feladatok kezelésének.

 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 waitgroup használatával valósítja meg, hogy minden goroutine egyidejűleg generál egy-egy véletlen egész számot 10 darabot, és ezeket a kiosztott indexre menti. Ebben a kódban a waitgroup segítségével megvárjuk, amíg az összes goroutine befejeződik, majd kiírjuk a Done szót. Ilyen módon a waitgroup használatával a goroutine-ok egyidejűleg dolgozhatnak, zár nélkül menthetik az adatokat, a feladatok befejezése után pedig egységesen elvégezhetjük az utófeldolgozást.

golang.org/x/sync/errgroup.ErrGroup

Az errgroup a sync.WaitGroup továbbfejlesztett csomagja. Az errgroup a sync.WaitGrouptól eltérően, ha a goroutine-ok feladatai közül bármelyikben hiba lép fel, megszakítja az összes goroutine-t, és visszaadja a hibát.

 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 goroutine-t hoz létre, és az 5. goroutine-ban hibát idéz elő. A szándékosan az ötödik goroutine-ban előidézett hiba segítségével mutattam be a hibás működés esetét. Valós használat során az errgroup-pal hozunk létre goroutine-okat, és az egyes goroutine-okban fellépő hibák esetén különféle utófeldolgozást végezhetünk.

once

Olyan eszköz, amely azt a kódot futtatja, amelynek csak egyszer szabad lefutnia. Az alábbi konstruktorokkal futtathatjuk a kapcsolódó 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

Az OnceFunc egyszerűen biztosítja, hogy az adott függvény az egész futás során pontosan egyszer hajtódjon végre.

 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 használatával létrehozunk egy once függvényt, és a once függvényt többszöri hívása esetén is a Hello, World! csak egyszer íródik ki.

OnceValue

Az OnceValue nem egyszerűen biztosítja, hogy az adott függvény az egész futás során pontosan egyszer hajtódjon végre, hanem eltárolja a függvény visszatérési értékét, és újrahíváskor az 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 segítségével a c változót eggyel növeli. Ebben a kódban a sync.OnceValue használatával létrehozunk egy once függvényt, és a once függvényt többszöri hívása esetén is a c változó csak egyszer növekszik, így az mindig az 1-es értéket adja vissza.

OnceValues

Az OnceValues az OnceValue-hoz hasonlóan működik, de több értéket is képes visszaadni.

 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 segítségével a c változót eggyel növeli. Ebben a kódban a sync.OnceValues használatával létrehozunk egy once függvényt, és a once függvényt többszöri hívása esetén is a c változó csak egyszer növekszik, így az mindig az 1-es értéket adja vissza.

atomic

Az atomic csomag atomi műveleteket biztosító csomag. Az atomic csomag az Add, CompareAndSwap, Load, Store, Swap stb. metódusokat kínálja, az újabb verziókban azonban az Int64, Uint64, Pointer stb. típusok használata javasolt.

 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 a példa már korábban is szerepelt. Az atomic.Int64 típus segítségével a c változót atomikusan növeljük. Az Add és Load metódusokkal atomikusan növelhetjük a változót, illetve beolvashatjuk annak értékét. Emellett a Store metódussal értéket tárolhatunk, a Swap metódussal értéket cserélhetünk, a CompareAndSwap metódussal pedig értéket hasonlíthatunk össze, és ha az megfelel, lecserélhetjük.

cond

sync.Cond

A cond csomag feltételváltozókat biztosító csomag. A cond csomag sync.Cond típussal hozható létre, és a Wait, Signal, Broadcast metódusokat kínálja.

 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 segítségével addig vár, amíg a ready változó értéke true nem lesz. Ebben a kódban a sync.Cond használatával addig várunk, amíg a ready változó értéke true nem lesz, majd kiírjuk a Ready! üzenetet. Ilyen módon a sync.Cond használatával több goroutine-t is várakoztathatunk addig, amíg azok egy adott feltételnek meg nem felelnek.

Ennek felhasználásával egyszerű queue is megvalósítható.

 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}

Ha így a sync.Cond-ot használjuk, a spin-lock által okozott nagy processzorhasználat helyett hatékonyan várakozhatunk, és ha a feltétel teljesül, újra folytathatjuk a működést.

semaphore

golang.org/x/sync/semaphore.Semaphore

A semaphore csomag szemafort biztosító csomag. A semaphore csomag a golang.org/x/sync/semaphore.Semaphore típussal hozható létre, és az Acquire, Release, TryAcquire metódusokat kínálja.

 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 a semaphore segítségével szemafort hoz létre, és az Acquire metódussal megszerzi, a Release metódussal pedig felszabadítja a szemafort. Ebben a kódban a semaphore használatával bemutattam a szemafor megszerzésének és felszabadításának módját.

Összegzés

Az alapvető tartalom ennyi elegendő lehet. A cikk anyagai alapján remélem, hogy megérti, hogyan kezelheti a párhuzamosságot goroutine-ok segítségével, és képes lesz azt a gyakorlatban is alkalmazni. Remélem, hogy ez a cikk hasznos volt az Ön számára. Köszönöm a figyelmet.