Go samanaikaisuus aloituspaketti
Yhteenveto
Lyhyt johdanto
Go-kielellä on monia samanaikaisuuden hallintaan tarkoitettuja työkaluja. Tässä artikkelissa esittelemme joitakin niistä ja niihin liittyviä temppuja.
Goroutiinit?
Goroutiini on Go-kielen tukema uusi samanaikaisuusmalli. Yleisesti ottaen ohjelmat suorittavat useita tehtäviä samanaikaisesti saamalla käyttöjärjestelmältä OS-säikeitä ja suorittamalla tehtäviä rinnakkain prosessoriytimien määrän mukaisesti. Pienempien samanaikaisuustasojen suorittamiseksi käyttäjätilassa luodaan green-säikeitä, ja yksi OS-säie suorittaa useita green-säikeitä vuorotellen. Goroutiinit ovat kuitenkin tehneet näistä green-säikeistä pienempiä ja tehokkaampia. Goroutiinit käyttävät vähemmän muistia kuin säikeet ja niitä voidaan luoda ja vaihtaa nopeammin kuin säikeitä.
Goroutiinien käyttö on yksinkertaista: riittää käyttää go
-avainsanaa. Tämä tekee ohjelmoinnista intuitiivisempaa, kun synkronista koodia suoritetaan asynkronisesti.
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}
Tämä koodi muuttaa yksinkertaisen, 1 sekunnin tauon jälkeen "Hello, World!" -tekstin tulostavan synkronisen koodin asynkroniseksi virraksi. Vaikka esimerkki on yksinkertainen, monimutkaisemman koodin muuttaminen synkronisesta asynkroniseksi parantaa koodin luettavuutta, näkyvyyttä ja ymmärrettävyyttä verrattuna async/await- tai promise-tyyppisiin menetelmiin.
Monissa tapauksissa kuitenkin yksinkertaisen asynkronisen kutsun ja fork & join
-tyyppisen virtauksen (joka muistuttaa jakoja valloita -menetelmää) ymmärtämättömyys johtaa huonoon goroutiinkoodiin. Esittelemme tässä artikkelissa joitakin tapoja ja tekniikoita tällaisten tilanteiden varalle.
Samanaikaisuuden hallinta
context
Ensimmäisenä hallintatekniikkana context
saattaa tuntua yllättävältä. Go-kielellä context
kuitenkin ylittää pelkän peruutusfunktionaalisuuden ja toimii erinomaisesti koko työpuun hallinnassa. Lyhyesti sanottuna, jos et ole perehtynyt tähän pakettiin, selitämme sen tässä.
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}
Yllä oleva koodi käyttää context
-pakettia tulostamaan "Context is done!" 1 sekunnin kuluttua. context
-paketin Done()
-metodi tarkistaa peruutuksen tilan, ja WithCancel
, WithTimeout
, WithDeadline
, WithValue
-metodit tarjoavat useita peruutustapoja.
Luodaan yksinkertainen esimerkki. Oletetaan, että kirjoitat koodia, joka hakee tietoja käyttämällä aggregator
-kuviota ja hakee tietoja user
, post
ja comment
-lähteistä. Jos kaikkien pyyntöjen tulee valmistua 2 sekunnissa, voit kirjoittaa koodin seuraavasti:
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}
Yllä oleva koodi tulostaa "Timeout!", jos kaikkia tietoja ei ole haettu 2 sekunnissa, ja "All data is fetched!", jos kaikki tiedot on haettu. context
-paketin avulla voit helposti hallita peruutuksia ja aikakatkaisuja koodissa, jossa toimii useita goroutiineja.
Lisätietoja context-funktiosta ja sen metodeista löytyy osoitteesta godoc context. Toivomme, että opit käyttämään niitä helposti.
channel
Puskurettomat kanavat
channel
on työkalu goroutiinien välisen viestinnän helpottamiseen. channel
luodaan make(chan T)
-funktiolla, missä T
on kanavan välittämän datan tyyppi. channel
-kanavan kautta dataa lähetetään ja vastaanotetaan <-
-operaattorilla ja kanava suljetaan close
-funktiolla.
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}
Yllä oleva koodi tulostaa 1 ja 2 käyttäen channel
-kanavaa. Tämä koodi näyttää vain yksinkertaisen datan lähettämisen ja vastaanottamisen kanavassa. channel
tarjoaa kuitenkin paljon enemmän toimintoja. Tutustumme ensin puskurettomiin ja puskuroituihin kanaviin. Ennen kuin aloitamme, huomaa, että yllä oleva esimerkki käyttää puskuretonta kanavaa, jossa datan lähettämisen ja vastaanottamisen on tapahtuttava samanaikaisesti. Jos näin ei ole, voi syntyä lukkiutuminen (deadlock).
Puskuroidut kanavat
Entä jos yllä oleva koodi ei ole yksinkertainen tulostus, vaan kaksi raskasta prosessia? Jos toisen prosessin lukeminen ja käsittely kestää kauan, ensimmäinen prosessi pysähtyy myös kyseiseksi ajaksi. Voimme välttää tämän käyttämällä puskuroitua kanavaa.
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}
Yllä oleva koodi tulostaa 1 ja 2 käyttäen puskuroitua kanavaa. Tässä koodissa puskuroitu kanava mahdollistaa datan lähettämisen ja vastaanottamisen tapahtumisen ilman samanaikaisuutta. Kanavan puskuri lisää tilaa, mikä estää myöhempien tehtävien aiheuttamia viiveitä.
select
select
-lauseketta käyttämällä voidaan helposti toteuttaa fan-in
-rakenne, kun käsitellään useita kanavia.
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}
Yllä oleva koodi luo kolme kanavaa, jotka välittävät 1, 2 ja 3 säännöllisesti, ja tulostaa arvot kanavista käyttäen select
-lauseketta. select
-lausekkeen avulla voidaan vastaanottaa dataa useista kanavista samanaikaisesti ja käsitellä dataa heti, kun se on saatavilla.
for range
channel
-kanavista dataa voidaan vastaanottaa helposti for range
-lausekkeella. Kun for range
-lauseketta käytetään kanavan kanssa, se toimii aina, kun kanavaan lisätään dataa, ja silmukka päättyy, kun kanava suljetaan.
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}
Yllä oleva koodi tulostaa 1 ja 2 käyttäen channel
-kanavaa. Tässä koodissa for range
-lauseke vastaanottaa ja tulostaa datan aina, kun kanavaan lisätään dataa. Silmukka päättyy, kun kanava suljetaan.
Tämä syntaksi sopii myös yksinkertaiseen synkronointiin, kuten jo aiemmin esitettiin:
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}
Tämä koodi tulostaa "Hello, World!" 1 sekunnin tauon jälkeen. channel
-kanavaa käyttämällä synkroninen koodi on muutettu asynkroniseksi ja join
-kohta on määritetty.
jne.
- Datan lähettäminen tai vastaanottaminen
nil
-kanavasta aiheuttaa äärettömän silmukan ja lukkiutumisen. - Datan lähettäminen suljettuun kanavaan aiheuttaa paniikin.
- Kanavaa ei tarvitse sulkea erikseen, roskienkerääjä sulkee sen.
mutex
spinlock
spinlock
on synkronointimenetelmä, jossa lukitusta yritetään toistuvasti silmukassa. Go-kielellä spinlock voidaan helposti toteuttaa osoittimien avulla.
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}
Yllä oleva koodi toteuttaa spinlock
-paketin. Koodi käyttää sync/atomic
-pakettia SpinLock
-rakenteen toteuttamiseen. Lock
-metodissa atomic.CompareAndSwapUintptr
yrittää asettaa lukituksen, ja Unlock
-metodissa atomic.StoreUintptr
poistaa lukituksen. Tämä menetelmä käyttää jatkuvasti prosessoria, kunnes lukitus saadaan, mikä voi johtaa äärettömään silmukkaan. Siksi spinlock
sopii parhaiten yksinkertaiseen synkronointiin tai tilanteisiin, joissa lukitus tarvitaan vain lyhyeksi ajaksi.
sync.Mutex
mutex
on työkalu goroutiinien synkronointiin. sync
-paketin tarjoama mutex
tarjoaa Lock
, Unlock
, RLock
ja RUnlock
-metodit. mutex
luodaan sync.Mutex
-rakenteella, ja lukemisen/kirjoittamisen lukitus voidaan toteuttaa sync.RWMutex
-rakenteella.
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}
Yllä olevassa koodissa kaksi goroutiinia yrittää käyttää samaa count
-muuttujaa lähes samanaikaisesti. mutex
tekee count
-muuttujan käyttöalueesta kriittisen alueen, mikä estää samanaikaisen käytön. Koodi tulostaa aina 2
, riippumatta siitä, kuinka monta kertaa sitä suoritetaan.
sync.RWMutex
sync.RWMutex
on mutex
, joka erottaa lukemisen ja kirjoittamisen lukitukset. RLock
ja RUnlock
-metodeilla voidaan asettaa ja poistaa lukemisen lukitus.
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}
Yllä oleva koodi toteuttaa ConcurrentMap
-rakenteen käyttäen sync.RWMutex
-rakennetta. Koodissa Get
-metodi asettaa lukemisen lukituksen ja Set
-metodi kirjoittamisen lukituksen, mikä takaa turvallisen pääsyn ja muokkaamisen data
-karttaan. Lukemisen lukitus on tarpeen, koska jos lukemista tapahtuu paljon, kirjoittamisen lukituksen sijasta voidaan käyttää lukemisen lukitusta, jolloin useat goroutiinit voivat lukea dataa samanaikaisesti. Tämä parantaa suorituskykyä, kun tilan muutosta ei tarvita ja kirjoittamisen lukitusta ei tarvita.
fakelock
fakelock
on yksinkertainen temppu, joka toteuttaa sync.Locker
-rajapinnan. Tämä rakenne tarjoaa samat metodit kuin sync.Mutex
, mutta se ei tee mitään.
1package fakelock
2
3type FakeLock struct{}
4
5func (f *FakeLock) Lock() {}
6
7func (f *FakeLock) Unlock() {}
Yllä oleva koodi toteuttaa fakelock
-paketin. Paketti toteuttaa sync.Locker
-rajapinnan ja tarjoaa Lock
ja Unlock
-metodit, mutta ne eivät tee mitään. Selitämme, miksi tämä koodi on hyödyllinen, myöhemmin.
waitgroup
sync.WaitGroup
sync.WaitGroup
on työkalu, joka odottaa, kunnes kaikkien goroutiinien tehtävät ovat valmiit. Se tarjoaa Add
, Done
ja Wait
-metodit. Add
lisää goroutiinien määrän, Done
ilmoittaa, että goroutiinin tehtävä on valmis, ja Wait
odottaa, kunnes kaikkien goroutiinien tehtävät ovat valmiit.
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}
Yllä oleva koodi on esimerkki siitä, kuinka sync.WaitGroup
-objektia käytetään 100:n goroutinen samanaikaiseen c
-muuttujan arvon lisäämiseen. Koodi odottaa sync.WaitGroup
:n avulla kaikkien goroutiinien päättymistä ennen kuin c
-muuttujan lisätty arvo tulostetaan. Jos kyseessä on vain muutamien tehtävien fork & join
-tyyppinen suoritus, kanavia voi riittää, mutta suuremmassa määrin fork & join
-tapauksessa sync.WaitGroup
on hyvä vaihtoehto.
with slice
Viipaleiden kanssa käytettynä waitgroup
on erinomainen työkalu samanaikaisten tehtävien hallintaan ilman lukitusmekanismeja.
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}
Yllä oleva koodi käyttää ainoastaan waitgroup
-objektia siten, että jokainen goroutiini luo samanaikaisesti 10 satunnaislukua ja tallentaa ne osoitettuun indeksiin. Koodi odottaa waitgroup
:n avulla kaikkien goroutiinien päättymistä ennen kuin tulostaa "Done". Tällä tavalla waitgroup
:a käyttämällä useat goroutiinit voivat suorittaa tehtäviä samanaikaisesti, tallentaa dataa lukituksetta ja suorittaa jälkikäsittelyn tehokkaasti tehtävien päätyttyä.
golang.org/x/sync/errgroup.ErrGroup
errgroup
on sync.WaitGroup
-paketin laajennus. Toisin kuin sync.WaitGroup
, errgroup
keskeyttää kaikki goroutiinit ja palauttaa virheen, jos jokin goroutiini tuottaa virheen.
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}
Yllä oleva koodi luo errgroup
:n avulla 10 goroutinia ja aiheuttaa virheen viidennessä goroutiinessa. Virheen aiheuttaminen viidennessä goroutiinessa on tarkoituksellista virhetilanteiden havainnollistamiseksi. Käytännön sovelluksissa errgroup
:a käytetään goroutiinien luomiseen ja erilaisten jälkikäsittelyjen suorittamiseen virhetilanteissa.
once
Työkalu koodin suorittamiseen vain kerran. Seuraavien konstrukttoreiden avulla voidaan suorittaa siihen liittyvää koodia.
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
varmistaa, että funktio suoritetaan vain kerran koko ohjelman ajon aikana.
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}
Yllä oleva koodi käyttää sync.OnceFunc
-funktiota "Hello, World!" -tulostukseen. Vaikka once
-funktiota kutsutaan useita kertoja, "Hello, World!" tulostuu vain kerran.
OnceValue
OnceValue
ei ainoastaan suorita funktiota vain kerran, vaan se tallentaa myös funktion palauttaman arvon ja palauttaa tallennetun arvon seuraavilla kutsulla.
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}
Yllä oleva koodi käyttää sync.OnceValue
-funktiota c
-muuttujan arvon lisäämiseen yhdellä. Vaikka once
-funktiota kutsutaan useita kertoja, c
-muuttujan arvo kasvaa vain kerran ja funktio palauttaa aina arvon 1.
OnceValues
OnceValues
toimii samalla tavalla kuin OnceValue
, mutta se voi palauttaa useita arvoja.
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}
Yllä oleva koodi käyttää sync.OnceValues
-funktiota c
-muuttujan arvon lisäämiseen yhdellä. Vaikka once
-funktiota kutsutaan useita kertoja, c
-muuttujan arvo kasvaa vain kerran ja funktio palauttaa aina arvon 1.
atomic
atomic
-paketti tarjoaa atomisia operaatioita. atomic
-paketti tarjoaa menetelmiä kuten Add
, CompareAndSwap
, Load
, Store
ja Swap
, mutta nykyään suositellaan Int64
, Uint64
ja Pointer
-tyyppien käyttöä.
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}
Tämä on aiempi esimerkki, jossa atomic.Int64
-tyyppiä käytetään c
-muuttujan atomisiin lisäyksiin. Add
- ja Load
-menetelmien avulla muuttujaa voidaan lisätä ja lukea atomisesti. Lisäksi Store
-menetelmällä voidaan tallentaa arvoja, Swap
-menetelmällä vaihtaa arvoja ja CompareAndSwap
-menetelmällä vertailla ja vaihtaa arvoja tarvittaessa.
cond
sync.Cond
cond
-paketti tarjoaa ehtojen muuttujia. cond
-paketti luodaan sync.Cond
:lla ja se tarjoaa Wait
, Signal
ja Broadcast
-menetelmiä.
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}
Yllä oleva koodi odottaa sync.Cond
:n avulla, että ready
-muuttujan arvoksi tulee true
. Koodi tulostaa "Ready!" kun ready
-muuttujan arvo on true
. sync.Cond
:n avulla useat goroutiinit voivat odottaa tietyn ehdon täyttymistä.
Tämä voidaan hyödyntää yksinkertaisen queue
:n toteutuksessa.
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}
sync.Cond
:n avulla voidaan odottaa tehokkaasti ehdon täyttymistä, mikä on tehokkaampaa kuin spin-lock
, joka kuluttaa paljon prosessorin resursseja.
semaphore
golang.org/x/sync/semaphore.Semaphore
semaphore
-paketti tarjoaa semaforeja. semaphore
-paketti luodaan golang.org/x/sync/semaphore.Semaphore
:lla ja se tarjoaa Acquire
, Release
ja TryAcquire
-menetelmät.
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}
Yllä oleva koodi luo semaforin ja hankkii sen Acquire
-menetelmän avulla ja vapauttaa sen Release
-menetelmän avulla. Koodi näyttää, kuinka semaforia hankitaan ja vapautetaan.
Lopuksi
Tämä kattaa perusteet. Toivottavasti tämä artikkeli auttaa teitä ymmärtämään ja käyttämään goroutiineja samanaikaisuuden hallinnassa. Toivottavasti tämä artikkeli oli hyödyllinen! Kiitos.