Go egyidejűségi kezdőcsomag
Á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
- Ha adatokat küldünk vagy fogadunk egy nil csatornán, végtelen ciklusba kerülhetünk, ami holtpontot okozhat.
- Ha adatokat küldünk egy lezárt csatornába, pánik keletkezik.
- 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.