GoSuda

Go-rinnakkaisuuden aloitussarja

By snowmerak
views ...

Yleiskatsaus

Lyhyt esittely

Go-kielessä on monia työkaluja samanaikaisuuden hallintaan. Tässä artikkelissa esittelemme joitakin niistä ja siihen liittyviä niksejä.

Goroutine?

goroutine on uudenlainen samanaikaisuusmalli, jota Go-kieli tukee. Tyypillisesti ohjelma vastaanottaa käyttöjärjestelmältä OS-säikeitä suorittaakseen useita tehtäviä samanaikaisesti, ja suorittaa tehtäviä rinnakkain ytimien lukumäärän mukaan. Pienemmän samanaikaisuuden suorittamiseksi käyttäjämaassa luodaan vihreitä säikeitä, jotta useat vihreät säikeet voivat vuorotellen suorittaa tehtäviä yhden OS-säikeen sisällä. goroutine-tapauksessa tällaiset vihreät säikeet on kuitenkin tehty entistä pienemmiksi ja tehokkaammiksi. Nämä goroutinet käyttävät vähemmän muistia kuin säikeet, ja ne voidaan luoda ja vaihtaa nopeammin kuin säikeet.

goroutinen käyttämiseksi riittää vain go-avaimen sanan käyttö. Tämä mahdollistaa synkronisen koodin suorittamisen asynkronisena koodina intuitiivisesti ohjelman kirjoitusprosessissa.

 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 tulostaa Hello, World! yhden sekunnin viiveen jälkeen, asynkroniseksi kuluksi. Vaikka tämä esimerkki on yksinkertainen, monimutkaisemman koodin muuttaminen synkronisesta asynkroniseksi parantaa koodin luettavuutta, näkyvyyttä ja ymmärrettävyyttä verrattuna olemassa oleviin async await- tai promise-menetelmiin.

Monissa tapauksissa kuitenkin, jos tätä synkronisen koodin asynkronista kutsumista ja fork & join -kaltaisia kulkuja (kuin jaa ja hallitse -tyyppisiä kulkuja) ei ymmärretä, voidaan luoda huonoja goroutine-koodeja. Tässä artikkelissa esittelemme joitakin tapoja ja tekniikoita, joilla voidaan varautua tällaisiin tapauksiin.

Samanaikaisuuden hallinta

context

Saattaa olla yllättävää, että context esiintyy ensimmäisenä hallintatekniikkana. Go-kielessä context kuitenkin ylittää pelkän peruutusominaisuuden ja sillä on erinomainen rooli koko tehtäväpuun hallinnassa. Selitän lyhyesti kyseisen paketin niille, jotka eivät sitä tunne.

 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 tulostaa Context is done! yhden sekunnin kuluttua käyttäen context-pakettia. context-paketista voi tarkistaa peruutuksen tilan Done()-metodin avulla, ja se tarjoaa erilaisia peruutusmenetelmiä WithCancel, WithTimeout, WithDeadline, WithValue -metodien kautta.

Luodaanpa yksinkertainen esimerkki. Oletetaan, että kirjoitat koodia, joka hakee user, post ja comment -tiedot aggregator-mallin avulla. Ja jos kaikki pyynnöt on tehtävä kahden sekunnin kuluessa, 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 voida hakea 2 sekunnin kuluessa, ja "All data is fetched!", jos kaikki tiedot on haettu. Tällä tavoin context-pakettia käyttämällä peruutusta ja aikakatkaisuja voidaan helposti hallita myös koodissa, jossa useita goroutineja toimii.

Tähän liittyviä erilaisia context-funktioita ja -metodeja on saatavilla godoc context -sivustolla. Toivottavasti opit ja voit hyödyntää niitä helposti.

channel

unbuffered channel

channel on työkalu goroutinejen väliseen kommunikaatioon. channel voidaan luoda komennolla make(chan T). Tässä T on sen datan tyyppi, jonka channel välittää. channel voi lähettää ja vastaanottaa dataa <- -operaattorilla, ja channel voidaan sulkea 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-mekanismia. Tämä koodi näyttää vain arvojen lähettämisen ja vastaanottamisen channel-mekanismin kautta. channel tarjoaa kuitenkin tätä enemmän toimintoja. Tutustutaan ensin buffered channel ja unbuffered channel -mekanismeihin. Ennen kuin aloitamme, yllä oleva esimerkki on unbuffered channel, ja kanavan datan lähettämisen ja vastaanottamisen on tapahduttava samanaikaisesti. Jos näin ei tapahdu, voi syntyä deadlock.

buffered channel

Entä jos yllä oleva koodi ei olisi pelkkä tulostus, vaan kaksi raskasta työtä suorittavaa prosessia? Jos toinen prosessi juuttuisi pitkäksi aikaa lukiessaan ja käsitellessään tietoa, myös ensimmäinen prosessi pysähtyisi samaksi ajaksi. Voimme käyttää buffered channel -mekanismia tämän tilanteen estämiseksi.

 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 channel -mekanismia. Tässä koodissa buffered channel -mekanismia käytetään varmistamaan, että datan lähettämisen ja vastaanottamisen channel -mekanismissa ei tarvitse tapahtua samanaikaisesti. Tällä tavoin, kun kanavassa on puskuri, saadaan tilaa puskurin pituuden verran, mikä estää viivästyksiä, jotka johtuvat myöhempien tehtävien vaikutuksesta.

select

Kun käsitellään useita kanavia, select-lauseen avulla voidaan helposti 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 välittävät säännöllisesti luvut 1, 2 ja 3, ja tulostaa kanavista vastaanotetut arvot select-lauseen avulla. Tällä tavoin select-lauseella voidaan vastaanottaa tietoa useista kanavista samanaikaisesti ja käsitellä tietoa heti, kun se saadaan kanavasta.

for range

channel-tyypin avulla voidaan helposti vastaanottaa dataa for range-rakenteen avulla. Kun for range-rakennetta käytetään kanavassa, 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ämällä channel-rakennetta. Tässä koodissa for range-rakennetta käytetään vastaanottamaan ja tulostamaan dataa aina, kun sitä lisätään kanavaan. Ja kun kanava suljetaan, silmukka päättyy.

Kuten useasti yllä on mainittu, tätä syntaksia voidaan käyttää myös yksinkertaisena synkronointimenetelmänä.

 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 tulostaa "Hello, World!" yhden sekunnin viiveen jälkeen. Tässä koodissa channel-mekanismia on käytetty synkronisen koodin muuntamiseen asynkroniseksi. Tällä tavoin channel-mekanismia käyttämällä synkroninen koodi voidaan helposti muuntaa asynkroniseksi ja join-pisteet voidaan määrittää.

etc

  1. Jos dataa lähetetään tai vastaanotetaan nil-kanavaan, voi syntyä dead-lock, joka johtaa äärettömään silmukkaan.
  2. Jos dataa lähetetään suljetulle kanavalle, tapahtuu panic.
  3. Kanavaa ei tarvitse sulkea erikseen, sillä GC sulkee sen kerätessään.

mutex

spinlock

spinlock on synkronointimenetelmä, joka yrittää jatkuvasti lukita toistamalla silmukkaa. Go-kielessä 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 on spinlock-paketin toteutus. Tässä koodissa SpinLock on toteutettu käyttämällä sync/atomic-pakettia. Lock-metodissa yritetään lukita käyttämällä atomic.CompareAndSwapUintptr-funktiota, ja Unlock-metodissa lukitus vapautetaan käyttämällä atomic.StoreUintptr-funktiota. Tämä menetelmä yrittää lukita taukoamatta, joten se kuluttaa jatkuvasti suorittimen aikaa, kunnes lukitus saadaan, mikä voi johtaa äärettömään silmukkaan. Siksi spinlock-mekanismia on suositeltavaa käyttää vain yksinkertaiseen synkronointiin tai lyhytaikaiseen käyttöön.

sync.Mutex

mutex on työkalu goroutinejen väliseen synkronointiin. sync-paketissa tarjolla oleva mutex tarjoaa metodit kuten Lock, Unlock, RLock, RUnlock. mutex voidaan luoda sync.Mutex-tyypillä, ja sync.RWMutex-tyyppiä voidaan käyttää luku-/kirjoituslukitukseen.

 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ävät lähes samanaikaisesti samaa count-muuttujaa. Tässä tapauksessa, jos count-muuttujan käyttöä koskeva koodi muutetaan kriittiseksi osaksi mutex-mekanismin avulla, samanaikainen pääsy count-muuttujaan voidaan estää. Tällöin tämä koodi tulostaa aina 2 riippumatta siitä, kuinka monta kertaa sitä suoritetaan.

sync.RWMutex

sync.RWMutex on mutex, jota voidaan käyttää erottelemalla lukitus lukemiseen ja kirjoittamiseen. Lukitus lukemiseen voidaan asettaa ja vapauttaa RLock- ja RUnlock-metodeilla.

 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ämällä sync.RWMutex-mekanismia. Tässä koodissa Get-metodi asettaa lukituslukon lukemista varten ja Set-metodi asettaa kirjoituslukon, jotta data-karttaan voidaan päästä ja sitä muokata turvallisesti. Lukituslukon tarve johtuu siitä, että jos on paljon yksinkertaisia lukutoimintoja, useat goroutinet voivat suorittaa lukutoimintoja samanaikaisesti asettamatta kirjoituslukkoa. Tällä tavoin suorituskykyä voidaan parantaa asettamalla vain lukituslukko niissä tapauksissa, joissa tilan muutosta ei tapahdu ja kirjoituslukkoa ei tarvita.

fakelock

fakelock on yksinkertainen temppu, joka toteuttaa sync.Locker-rajapinnan. Tämä rakenne tarjoaa samat metodit kuin sync.Mutex, mutta se ei suorita varsinaisia toimintoja.

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.Locker-rajapinnan ja tarjoaa Lock- ja Unlock-metodit, mutta se ei itse asiassa tee mitään. Selitän miksi tällaista koodia tarvitaan, jos tilaisuus tulee.

waitgroup

sync.WaitGroup

sync.WaitGroup on työkalu, joka odottaa kaikkien goroutine-tehtävien valmistumista. Se tarjoaa metodit Add, Done ja Wait. Add-metodilla lisätään goroutinejen lukumäärä, Done-metodilla ilmoitetaan goroutine-tehtävän päättymisestä. Ja Wait-metodilla odotetaan kaikkien goroutine-tehtävien valmistumista.

 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.WaitGroup-mekanismia, jotta 100 goroutinea lisäävät samanaikaisesti arvoja c-muuttujaan. Tässä koodissa sync.WaitGroup-mekanismia käytetään odottamaan, kunnes kaikki goroutinet ovat valmiita, ja sitten tulostamaan c-muuttujaan lisätty arvo. Vaikka pelkkä kanava riittää muutaman tehtävän fork & join -toimintoon, sync.WaitGroup-mekanismin käyttö on hyvä vaihtoehto, kun kyseessä on suuri määrä tehtäviä.

with slice

Kun sitä käytetään yhdessä slicen kanssa, waitgroup voi olla erinomainen työkalu samanaikaisten suoritusten hallintaan ilman lukitusta.

 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-mekanismia, jotta jokainen goroutine luo samanaikaisesti 10 satunnaista kokonaislukua ja tallentaa ne osoitettuun indeksiin. Tässä koodissa waitgroup-mekanismia käytetään odottamaan, kunnes kaikki goroutinet ovat valmiita, ja sitten tulostetaan "Done". Tällä tavoin waitgroup-mekanismia käyttämällä useat goroutinet voivat suorittaa tehtäviä samanaikaisesti, tallentaa dataa ilman lukitusta, kunnes kaikki goroutinet ovat valmiita, ja suorittaa jälkikäsittelyn yhtenäisesti tehtävän päätyttyä.

golang.org/x/sync/errgroup.ErrGroup

errgroup on paketti, joka laajentaa sync.WaitGroup-mekanismia. Toisin kuin sync.WaitGroup, errgroup peruuttaa kaikki goroutinet ja palauttaa virheen, jos yksikin goroutine-tehtävä epäonnistuu.

 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 10 goroutinea errgroup-mekanismin avulla ja aiheuttaa virheen viidennessä goroutinessa. Tarkoituksena oli näyttää tapaus, jossa virhe tapahtuu. Kuitenkin todellisessa käytössä errgroup-mekanismia käytetään luomaan goroutineja, ja erilaisia jälkikäsittelyjä suoritetaan, jos virheitä ilmenee kussakin goroutinessa.

once

Työkalu, joka suorittaa koodin vain kerran. Voit suorittaa siihen liittyvän koodin alla olevan konstruktorin kautta.

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ä kyseinen funktio suoritetaan vain kerran koko suorituksen 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 tulostaa "Hello, World!" käyttäen sync.OnceFunc-mekanismia. Tässä koodissa sync.OnceFunc-mekanismia käytetään luomaan once-funktio, ja vaikka once-funktiota kutsuttaisiin useita kertoja, "Hello, World!" tulostetaan vain kerran.

OnceValue

OnceValue ei ainoastaan suorita kyseistä funktiota vain kerran kokonaisuudessaan, vaan tallentaa myös 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 lisää c-muuttujaa yhdellä käyttämällä sync.OnceValue-mekanismia. Tässä koodissa sync.OnceValue-mekanismia käytetään luomaan once-funktio, ja vaikka once-funktiota kutsuttaisiin useita kertoja, c-muuttuja palauttaa vain kerran kasvaneen 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 lisää c-muuttujaa yhdellä käyttämällä sync.OnceValues-mekanismia. Tässä koodissa sync.OnceValues-mekanismia käytetään luomaan once-funktio, ja vaikka once-funktiota kutsuttaisiin useita kertoja, c-muuttuja palauttaa vain kerran kasvaneen arvon 1.

atomic

atomic-paketti tarjoaa atomisia operaatioita. atomic-paketti tarjoaa metodeja kuten Add, CompareAndSwap, Load, Store, Swap, mutta viime aikoina on suositeltu käyttämään tyyppejä kuten 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}

Yllä oleva esimerkki on koodi, joka lisää c-muuttujan atomisesti käyttämällä atomic.Int64-tyyppiä. Add-metodilla ja Load-metodilla voidaan lisätä muuttujaa atomisesti ja lukea muuttujan arvo. Lisäksi Store-metodilla voidaan tallentaa arvo, Swap-metodilla voidaan vaihtaa arvo, ja CompareAndSwap-metodilla voidaan verrata arvoja ja vaihtaa ne, jos ne ovat sopivia.

cond

sync.Cond

cond-paketti tarjoaa ehtomuuttujia. cond-paketti voidaan luoda sync.Cond-tyypillä, ja se tarjoaa metodit 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}

Yllä oleva koodi odottaa, kunnes ready-muuttuja on true, käyttäen sync.Cond-mekanismia. Tässä koodissa sync.Cond-mekanismia käytetään odottamaan, kunnes ready-muuttuja on true, ja sitten tulostamaan "Ready!". Tällä tavoin sync.Cond-mekanismia käyttämällä useat goroutinet voidaan saada odottamaan samanaikaisesti, kunnes tietty ehto täyttyy.

Tätä hyödyntäen voidaan toteuttaa yksinkertainen queue.

 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}

Tällä tavoin sync.Cond-mekanismia hyödyntämällä voidaan tehokkaasti odottaa ja toimia uudelleen, kun ehto täyttyy, sen sijaan että käytettäisiin paljon suorittimen aikaa spin-lock-mekanismin kanssa.

semaphore

golang.org/x/sync/semaphore.Semaphore

semaphore-paketti tarjoaa semaforin. semaphore-paketti voidaan luoda golang.org/x/sync/semaphore.Semaphore-tyypillä, ja se tarjoaa metodit 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}

Yllä oleva koodi luo semaforin semaphore-mekanismin avulla ja näyttää, kuinka semafori hankitaan Acquire-metodilla ja vapautetaan Release-metodilla. Tässä koodissa esiteltiin tapa hankkia ja vapauttaa semafori semaphore-mekanismin avulla.

Lopuksi

Perusasiat riittänevät tähän. Tämän artikkelin pohjalta toivon, että ymmärrätte, kuinka goroutineja käytetään samanaikaisuuden hallintaan ja että voitte hyödyntää niitä käytännössä. Toivon, että tämä artikkeli oli teille hyödyllinen. Kiitos.