Go concurrency -aloituspaketti

Yleiskatsaus
Lyhyt johdanto
Go-kielessä on monia työkaluja samanaikaisuuden hallintaan. Tässä artikkelissa esittelen osan niistä sekä joitakin temppuja.
Goroutine?
goroutine on Go-kielen tukema uusi muoto samanaikaisuusmallista. Yleensä ohjelma saa käyttöjärjestelmältä OS-säikeitä suorittaakseen useita tehtäviä samanaikaisesti, ja suorittaa niitä rinnakkain ytimien määrän verran. Pienemmän mittakaavan samanaikaisuuden toteuttamiseksi luodaan käyttäjätilassa green threadeja, jolloin useat green threadit suorittavat tehtäviä vuorotellen yhden OS-säikeen sisällä. Goroutinet ovat kuitenkin tehneet tästä green thread -muodosta entistä pienemmän ja tehokkaamman. Goroutinet käyttävät vähemmän muistia kuin säikeet, ja niitä voidaan luoda ja vaihtaa nopeammin kuin säikeitä.
Goroutinea käytetään yksinkertaisesti avainsanalla go. Tämä mahdollistaa synkronisen koodin suorittamisen intuitiivisesti asynkronisena ohjelmoinnin aikana.
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 synkronisen koodin, joka odottaa sekunnin ja tulostaa Hello, World!, asynkroniseksi kulkuun. Vaikka tämä esimerkki on yksinkertainen, kun monimutkaisempi koodi muutetaan synkronisesta asynkroniseksi, koodin luettavuus, näkyvyys ja ymmärrettävyys paranevat verrattuna perinteisiin tapoihin, kuten async await tai promise.
Kuitenkin monissa tapauksissa, jos ei ymmärretä pelkästään synkronisen koodin kutsumista asynkronisesti ja fork & join -tyyppistä kulkua (joka muistuttaa divide and conquer -menetelmää), syntyy huonoja goroutine-koodeja. Tähän tilanteeseen voi varautua tässä artikkelissa esiteltävillä menetelmillä ja tekniikoilla.
Samanaikaisuuden hallinta
context
Saattaa olla yllättävää, että ensimmäinen hallintatekniikka on context. Go-kielessä context ylittää kuitenkin pelkän peruutustoiminnon ja toimii erinomaisesti koko tehtäväpuun hallinnassa. Jos paketti on jollekulle tuntematon, selitän sen lyhyesti.
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ää contextia ja tulostaa Context is done! yhden sekunnin kuluttua. context tarjoaa tiedon peruutuksesta Done()-metodin kautta, ja tarjoaa erilaisia peruutustapoja metodien WithCancel, WithTimeout, WithDeadline ja WithValue kautta.
Tehdään yksinkertainen esimerkki. Oletetaan, että haluat hakea dataa aggregator-mallilla ja kirjoittaa koodin, joka hakee user, post ja comment. Jos kaikkien pyyntöjen on tapahduttava 2 sekunnin sisällä, voit kirjoittaa sen 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 saada haettua 2 sekunnin sisällä, ja tulostaa All data is fetched!, jos kaikki tiedot haetaan. Näin contextia käyttämällä voidaan helposti hallita peruutuksia ja aikakatkaisuja myös koodissa, jossa useat goroutinet toimivat.
Lisää context-funktioita ja -metodeja löytyy godoc context-sivulta. Toivon, että opit yksinkertaiset asiat ja pystyt käyttämään niitä vaivattomasti.
channel
unbuffered channel
channel on työkalu goroutinejen väliseen kommunikaatioon. channelin voi luoda make(chan T)-funktiolla. Tässä T on tyyppi, jota kanava välittää. Dataa lähetetään ja vastaanotetaan <--operaattorilla, ja channel 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 channelia. Tässä koodissa näytetään vain yksinkertainen arvon lähettäminen ja vastaanottaminen kanavaan. channel tarjoaa kuitenkin enemmän ominaisuuksia. Tutustutaan ensin buffered channel- ja unbuffered channel-kanaviin. Ennen kuin jatkamme, huomaa että yllä oleva esimerkki käyttää unbuffered channelia, jossa kanavaan lähettäminen ja siitä vastaanottaminen on tapahduttava samanaikaisesti. Jos näin ei tapahdu, voi syntyä deadlock.
buffered channel
Entä jos yllä oleva koodi ei vain tulostaisi, vaan suorittaisi kaksi raskasta prosessia? Jos toinen prosessi lukee ja käsittelee dataa ja jumittuu pitkäksi aikaa, myös ensimmäinen prosessi pysähtyy siksi ajaksi. Tämän tilanteen välttämiseksi voimme käyttää buffered channelia.
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 buffered channelia. Tässä koodissa buffered channelin avulla kanavaan lähettäminen ja siitä vastaanottaminen eivät välttämättä tapahdu samanaikaisesti. Kun kanavalla on puskuri, siinä on varaa puskurin koon verran, joten myöhemmän tehtävän aiheuttamia viiveitä voidaan välttää.
select
Kun käsitellään useita kanavia, select-syntaksilla on helppo toteuttaa fan-in-rakenne.
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 lähettävät säännöllisesti arvot 1, 2 ja 3, ja tulostaa kanavista vastaanotetut arvot selectin avulla. Näin selectillä voidaan vastaanottaa dataa useilta kanavilta samanaikaisesti ja käsitellä arvoja heti, kun ne saapuvat.
for range
channelia voi lukea helposti for range-silmukalla. Kun for rangeia käytetään kanavaan, se suoritetaan 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 channelia. Tässä koodissa for range vastaanottaa ja tulostaa dataa aina, kun kanavaan lisätään dataa. Kun kanava suljetaan, silmukka päättyy.
Kuten edellä useaan kertaan kirjoitettiin, tätä syntaksia voidaan käyttää myös yksinkertaisena synkronointikeinona.
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}
Yllä oleva koodi odottaa yhden sekunnin ja tulostaa Hello, World!. Tässä koodissa synkroninen koodi on muutettu asynkroniseksi käyttämällä channelia. Näin channelia käyttämällä synkronisen koodin voi helposti muuttaa asynkroniseksi ja määrittää join-kohdan.
etc
- Jos nil-kanavaan lähetetään dataa tai siltä vastaanotetaan dataa, ohjelma voi jäädä ikuiseen silmukkaan ja deadlock voi syntyä.
- Jos kanavaan lähetetään dataa sen jälkeen, kun se on suljettu, syntyy panic.
- Vaikka kanavaa ei sulkisi erikseen, GC kerää sen ja sulkee kanavan.
mutex
spinlock
spinlock on synkronointimenetelmä, joka yrittää lukita toistuvasti silmukassa. Go-kielessä spinlockin voi toteuttaa helposti käyttämällä osoittimia.
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 on spinlock-paketin toteutus. Tässä koodissa SpinLock on toteutettu sync/atomic-paketilla. Lock-metodi yrittää lukitusta atomic.CompareAndSwapUintptr-funktiolla, ja Unlock-metodi vapauttaa lukon atomic.StoreUintptr-funktiolla. Koska tämä menetelmä yrittää lukitusta jatkuvasti, se käyttää CPU:ta, kunnes lukko saadaan, ja voi joutua ikuiseen silmukkaan. Siksi spinlockia kannattaa käyttää vain yksinkertaiseen synkronointiin tai lyhyeksi ajaksi.
sync.Mutex
mutex on työkalu goroutinejen väliseen synkronointiin. sync-paketin mutex tarjoaa metodit Lock, Unlock, RLock, RUnlock ja muita. mutex luodaan sync.Mutex-tyypillä, ja sync.RWMutex-tyypillä voi käyttää luku/kirjoitus-lukkoa.
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 goroutinea käyttää lähes samanaikaisesti samaa count-muuttujaa. Kun count-muuttujaan käsiksi pääsevä koodi tehdään kriittiseksi alueeksi mutexin avulla, voidaan estää count-muuttujan samanaikainen käyttö. Silloin tämä koodi tulostaa aina 2, riippumatta siitä, montako kertaa sen suorittaa.
sync.RWMutex
sync.RWMutex on mutex, jossa luku- ja kirjoituslukot voidaan erottaa toisistaan. Luku-lukko voidaan asettaa ja vapauttaa metodeilla RLock ja 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}
Yllä oleva koodi toteuttaa ConcurrentMapin käyttäen sync.RWMutexia. Tässä koodissa Get-metodi asettaa luku-lukon ja Set-metodi kirjoitus-lukon, joten data-karttaan voidaan käsiksi turvallisesti lukea ja muokata. Luku-lukko on tarpeen, kun lukuoperaatioita on paljon: sen sijaan että kirjoitus-lukko asetettaisiin, asetetaan pelkkä luku-lukko, jolloin useat goroutinet voivat lukea samanaikaisesti. Näin, jos tilaa ei muuteta eikä kirjoitus-lukkoa tarvita, pelkkä luku-lukko parantaa suorituskykyä.
fakelock
fakelock on yksinkertainen temppu, joka toteuttaa sync.Lockerin. Tämä rakenne tarjoaa samat metodit kuin sync.Mutex, mutta ei todellisuudessa tee mitään.
1package fakelock
2
3type FakeLock struct{}
4
5func (f *FakeLock) Lock() {}
6
7func (f *FakeLock) Unlock() {}
Yllä oleva koodi on fakelock-paketin toteutus. Tämä paketti toteuttaa sync.Lockerin ja tarjoaa Lock- ja Unlock-metodit, mutta ne eivät todellisuudessa tee mitään. Miksi tällaista koodia tarvitaan, selitän myöhemmin, kun siihen on sopiva tilaisuus.
waitgroup
sync.WaitGroup
sync.WaitGroup on työkalu, joka odottaa, että kaikki goroutinejen tehtävät ovat valmiita. Se tarjoaa metodit Add, Done ja Wait. Add-metodilla lisätään goroutinejen määrää, Done-metodilla ilmoitetaan goroutinen tehtävän valmistumisesta, ja Wait-metodilla odotetaan, että kaikkien goroutinejen tehtävät ovat valmiita.
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 käyttää sync.WaitGroupia, jolloin 100 goroutinea lisää samanaikaisesti arvoa muuttujaan c. Tässä koodissa sync.WaitGroupia käytetään odottamaan, että kaikki goroutinet ovat valmiita, minkä jälkeen tulostetaan muuttujaan c kertynyt arvo. Jos fork & join koskee vain muutamaa tehtävää, pelkkä kanava riittää, mutta suuren määrän tehtäviä fork & join-kuviossa sync.WaitGroup on hyvä vaihtoehto.
Käyttö slicen kanssa
Kun sitä käytetään yhdessä slicen kanssa, waitgroup voi olla erinomainen työkalu samanaikaisten tehtävien hallintaan ilman lukkoja.
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ää pelkkää waitgroupia, jolloin jokainen goroutine luo samanaikaisesti satunnaisen kokonaisluvun ja tallentaa sen sille osoitettuun indeksiin. Tässä koodissa waitgroupia käytetään odottamaan, että kaikki goroutinet ovat valmiita, minkä jälkeen tulostetaan Done. Näin waitgroupia käyttämällä useat goroutinet voivat suorittaa tehtäviä samanaikaisesti, tallentaa dataa ilman lukkoa, ja tehdä yhteisen jälkikäsittelyn kaikkien valmistuttua.
golang.org/x/sync/errgroup.ErrGroup
errgroup on sync.WaitGroupin laajennus. Toisin kuin sync.WaitGroup, errgroup peruuttaa kaikki goroutinet ja palauttaa virheen, jos jokin goroutinen tehtävä aiheuttaa 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 errgroupia käyttäen 10 goroutinea, joista viides aiheuttaa virheen. Aiheutin virheen tarkoituksella viidennessä goroutinessa, jotta näytän virhetilanteen. Käytännössä errgroupia käytetään siten, että goroutineja luodaan ja jokaisessa goroutinessa voidaan toteuttaa erilaisia jälkikäsittelyjä virhetilanteissa.
once
Työkalu, joka suorittaa koodin, jonka pitäisi suoriutua vain kerran. Alla olevilla konstruktoreilla voit suorittaa siihen liittyvän koodin.
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 mahdollistaa yksinkertaisesti, että funktio suoritetaan koko ohjelman ajan vain kerran.
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 tulostaa Hello, World! käyttäen sync.OnceFuncia. Tässä koodissa luodaan once-funktio sync.OnceFuncilla, ja vaikka once-funktiota kutsutaan useita kertoja, Hello, World! tulostuu vain kerran.
OnceValue
OnceValue ei yksinkertaisesti suorita funktiota vain kerran, vaan tallentaa funktion palautusarvon ja palauttaa tallennetun arvon, kun sitä kutsutaan uudelleen.
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 kasvattaa muuttujaa c yhdellä käyttäen sync.OnceValuea. Tässä koodissa luodaan once-funktio sync.OnceValuella, ja vaikka once-funktiota kutsutaan useita kertoja, muuttuja c kasvaa vain kerran ja palautetaan arvo 1.
OnceValues
OnceValues toimii samoin kuin OnceValue, mutta 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 kasvattaa muuttujaa c yhdellä käyttäen sync.OnceValuesa. Tässä koodissa luodaan once-funktio sync.OnceValueslla, ja vaikka once-funktiota kutsutaan useita kertoja, muuttuja c kasvaa vain kerran ja palautetaan arvo 1.
atomic
atomic-paketti tarjoaa atomisia operaatioita. Se tarjoaa metodit Add, CompareAndSwap, Load, Store, Swap ja muita, mutta viime aikoina 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 esimerkki, joka käytettiin aiemmin. Koodi kasvattaa muuttujaa c atomisesti käyttäen atomic.Int64-tyyppiä. Add- ja Load-metodeilla voidaan kasvattaa muuttujaa atomisesti ja lukea sen arvo. Lisäksi Store-metodilla voidaan tallentaa arvo, Swap-metodilla vaihtaa arvo ja CompareAndSwap-metodilla verrata arvoa ja vaihtaa se, jos se sopii.
cond
sync.Cond
cond-paketti tarjoaa ehtomuuttujia. cond-paketti voidaan luoda sync.Condilla, ja se tarjoaa metodit Wait, Signal ja 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}
Yllä oleva koodi odottaa, että ready-muuttuja muuttuu arvoon true, käyttäen sync.Condia. Tässä koodissa sync.Condia käytetään odottamaan, kunnes ready-muuttuja on true, ja sen jälkeen tulostetaan Ready!. Näin sync.Condia käyttämällä useat goroutinet voivat odottaa, kunnes tietty ehto täyttyy.
Tätä voidaan hyödyntää yksinkertaisen queuen toteuttamiseen.
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}
Kun sync.Condia käytetään tällä tavalla, sen sijaan että käytettäisiin paljon CPU:ta spin-lockilla, odotetaan tehokkaasti, ja kun ehto täyttyy, koodi voi jatkaa toimintaansa.
semaphore
golang.org/x/sync/semaphore.Semaphore
semaphore-paketti tarjoaa semafooreja. semaphore-paketti voidaan luoda golang.org/x/sync/semaphore.Semaphorella, ja se tarjoaa metodit Acquire, Release ja 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}
Yllä oleva koodi luo semaforin käyttäen semaphorea, hankkii sen Acquire-metodilla ja vapauttaa Release-metodilla. Tässä koodissa näytettiin, miten semaforia hankitaan ja vapautetaan semaphore-paketilla.
Lopuksi
Perusasiat riittävät tähän asti, luulen. Toivon, että artikkelin sisällön perusteella ymmärrät, miten goroutinejen samanaikaisuutta hallitaan, ja pystyt käyttämään sitä käytännössä. Toivon, että tämä artikkeli on ollut sinulle hyödyllinen. Kiitos.