---
id: 01933e13f963942e4c9bc8c3c89fbc2b
author: snowmerak
title: Štartovací balíček pre súbežnosť v Go
description: Predstavujeme rôzne nástroje a techniky na správu súbežnosti v jazyku Go, vrátane goroutín, kanálov, mutexov a ďalších.
language: sk
date: 2024-10-18T05:51:30.010680581Z
path: /blog/posts/go-concurrency-starter-pack-z221a3399
category: concurrency
---

## Prehľad

### Krátky úvod

Jazyk Go poskytuje mnoho nástrojov na správu súbežnosti. V tomto článku predstavím niektoré z nich a niekoľko trikov.

### Goroutines?

Goroutine je nová forma modelu súbežnosti podporovaného jazykom Go. Program zvyčajne získa od OS vlákna OS, aby mohol súčasne vykonávať viacero úloh, a paralelne ich vykonáva v počte zodpovedajúcom počtu jadier. Na dosiahnutie súbežnosti v menšom rozsahu sa v používateľskom priestore (userland) vytvárajú zelené vlákna (green threads), ktoré v rámci jedného vlákna OS striedavo vykonávajú úlohy. Goroutine však túto formu zelených vlákien navrhli ešte menšie a efektívnejšie. Goroutine využívajú menej pamäte ako vlákna a možno ich vytvárať a prepínať rýchlejšie než vlákna.

Na používanie goroutine stačí jednoducho použiť kľúčové slovo `go`. To umožňuje intuitívne spúšťať synchrónny kód ako asynchrónny počas písania programu.

```go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan struct{})
    go func() {
        defer close(ch)
        time.Sleep(1 * time.Second)
        fmt.Println("Hello, World!")
    }()

    fmt.Println("Waiting for goroutine...")
    for range ch {}
}
```

Tento kód jednoducho mení synchrónny kód, ktorý po sekunde pozastavenia vypíše `Hello, World!`, na asynchrónny tok. Hoci je tento príklad jednoduchý, pri premene zložitejšieho kódu zo synchrónneho na asynchrónny sa čitateľnosť, prehľadnosť a zrozumiteľnosť kódu oproti doterajším spôsobom, ako sú async await alebo promise, zlepšia.

Vo veľa prípadoch sa však bez pochopenia jednoduchého asynchrónneho volania takéhoto synchrónneho kódu a tokov typu `fork & join` (podobných rozdeľuj a panuj) môže vytvoriť zlý kód goroutine. Na riešenie takýchto prípadov predstavím v tomto článku niekoľko metód a techník.

## Správa súbežnosti

### context

Že ako prvú techniku správy uvádzam `context`, môže byť prekvapujúce. V jazyku Go však `context` presahuje jednoduchú funkciu zrušenia a výrazne sa uplatňuje pri správe celého stromu úloh. Pre tých, ktorí tento balík nepoznajú, ho stručne vysvetlím.

```go
package main

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func() {
        <-ctx.Done()
        fmt.Println("Context is done!")
    }()

    time.Sleep(1 * time.Second)

    cancel()

    time.Sleep(1 * time.Second)
}
```

Uvedený kód pomocou `context` vypíše `Context is done!` po jednej sekunde. Stav zrušenia možno zistiť metódou `Done()` a metódy ako `WithCancel`, `WithTimeout`, `WithDeadline` alebo `WithValue` poskytujú rôzne spôsoby zrušenia.

Vytvorme jednoduchý príklad. Predpokladajme, že na získanie dát používate vzor `aggregator` a píšete kód, ktorý získava `user`, `post` a `comment`. Ak musia všetky požiadavky prebehnúť do 2 sekúnd, môžete napísať nasledujúci kód.

```go
package main

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
    defer cancel()

    ch := make(chan struct{})
    go func() {
        defer close(ch)
        user := getUser(ctx)
        post := getPost(ctx)
        comment := getComment(ctx)

        fmt.Println(user, post, comment)
    }()

    select {
    case <-ctx.Done():
        fmt.Println("Timeout!")
    case <-ch:
        fmt.Println("All data is fetched!")
    }
}
```

Uvedený kód vypíše `Timeout!`, ak sa všetky dáta nepodarí získať do 2 sekúnd, a ak sa všetky dáta získajú, vypíše `All data is fetched!`. Týmto spôsobom možno pomocou `context` jednoducho spravovať zrušenie a časové limity aj v kóde, ktorý spúšťa viacero goroutine.

Rôzne funkcie a metódy súvisiace s context možno nájsť na stránke [godoc context](https://pkg.go.dev/context). Dúfam, že si jednoduché veci osvojíte a budete ich môcť pohodlne používať.

### channel

#### unbuffered channel

`channel` je nástroj na komunikáciu medzi goroutine. `channel` možno vytvoriť pomocou `make(chan T)`. Pritom `T` je typ dát, ktoré daný `channel` prenáša. Dáta sa posielajú a prijímajú pomocou `<-` a `channel` možno zatvoriť pomocou `close`.

```go
package main

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2
        close(ch)
    }()

    for i := range ch {
        fmt.Println(i)
    }
}
```

Uvedený kód pomocou `channel` vypíše 1 a 2. Tento kód ukazuje iba jednoduché posielanie a prijímanie hodnôt cez `channel`, no `channel` poskytuje viac funkcií. Najprv si priblížim `buffered channel` a `unbuffered channel`. Predtým však treba uviesť, že uvedený príklad používa `unbuffered channel`, pri ktorom sa musí odoslanie dát do kanála a ich prijatie uskutočniť súčasne. Ak sa tieto akcie nevykonajú súčasne, môže dôjsť k deadlocku.

#### buffered channel

Čo ak uvedený kód nevykonáva jednoduchý výpis, ale dva procesy s náročnou úlohou? Ak druhý proces prijíma dáta a dlhodobo sa zdržiava spracovaním, zastaví sa aj prvý proces na tento čas. Aby sme tomu zabránili, môžeme použiť `buffered channel`.

```go
package main

func main() {
    ch := make(chan int, 2)
    go func() {
        ch <- 1
        ch <- 2
        close(ch)
    }()

    for i := range ch {
        fmt.Println(i)
    }
}
```

Uvedený kód pomocou `buffered channel` vypíše 1 a 2. V tomto kóde sa použitím `buffered channel` dosiahlo, že odoslanie dát do `channel` a ich prijatie nemusia prebiehať súčasne. Keď má kanál buffer, vzniká rezerva zodpovedajúca jeho dĺžke, vďaka čomu možno zabrániť oneskoreniu úloh spôsobenému vplyvom neskorších úloh.

#### select

Pri práci s viacerými kanálmi možno pomocou príkazu `select` jednoducho implementovať štruktúru `fan-in`.

```go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int, 10)
    ch2 := make(chan int, 10)
    ch3 := make(chan int, 10)

    go func() {
        for {
            ch1 <- 1
            time.Sleep(1 * time.Second)
        }
    }()
    go func() {
        for {
            ch2 <- 2
            time.Sleep(2 * time.Second)
        }
    }()
    go func() {
        for {
            ch3 <- 3
            time.Sleep(3 * time.Second)
        }
    }()

    for i := 0; i < 3; i++ {
        select {
        case v := <-ch1:
            fmt.Println(v)
        case v := <-ch2:
            fmt.Println(v)
        case v := <-ch3:
            fmt.Println(v)
        }
    }
}
```

Uvedený kód vytvára tri kanály, ktoré periodicky posielajú hodnoty 1, 2 a 3, a pomocou `select` prijíma hodnoty z kanálov a vypisuje ich. Týmto spôsobom možno pomocou `select` súčasne prijímať dáta z viacerých kanálov a spracovávať ich podľa toho, ako prichádzajú.

#### for range

`channel` možno jednoducho používať na prijímanie dát pomocou `for range`. Keď sa `for range` použije na kanál, vykoná sa vždy, keď sa do kanála pridajú dáta, a keď sa kanál zatvorí, slučka sa ukončí.

```go
package main

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2
        close(ch)
    }()

    for i := range ch {
        fmt.Println(i)
    }
}
```

Uvedený kód pomocou `channel` vypíše 1 a 2. V tomto kóde sa pomocou `for range` pri každom pridaní dát do kanála dáta prijmú a vypíšu. Keď sa kanál zatvorí, slučka sa ukončí.

Ako som uviedol vyššie, túto syntax možno použiť aj ako jednoduchý synchronizačný prostriedok.

```go
package main

func main() {
    ch := make(chan struct{})
    go func() {
        defer close(ch)
        time.Sleep(1 * time.Second)
        fmt.Println("Hello, World!")
    }()

    fmt.Println("Waiting for goroutine...")
    for range ch {}
}
```

Uvedený kód po sekunde pozastavenia vypíše `Hello, World!`. V tomto kóde sa pomocou `channel` zmenil synchrónny kód na asynchrónny. Týmto spôsobom možno pomocou `channel` jednoducho previesť synchrónny kód na asynchrónny a nastaviť bod `join`.

#### etc

1. Ak sa do nil kanála pošlú alebo z neho prijmú dáta, môže sa dostať do nekonečnej slučky a vzniknúť deadlock.
2. Ak sa po zatvorení kanála pošlú dáta, vznikne panic.
3. Ani keď kanál explicitne nezatvoríte, GC ho pri uvoľňovaní pamäte zatvorí.

### mutex

#### spinlock

`spinlock` je synchronizačná metóda, ktorá v cykle opakovane pokúša získať zámok. V jazyku Go možno spinlock jednoducho implementovať pomocou ukazovateľov.

```go
package spinlock

import (
    "runtime"
    "sync/atomic"
)

type SpinLock struct {
    lock uintptr
}

func (s *SpinLock) Lock() {
    for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) {
        runtime.Gosched()
    }
}

func (s *SpinLock) Unlock() {
    atomic.StoreUintptr(&s.lock, 0)
}

func NewSpinLock() *SpinLock {
    return &SpinLock{}
}
```

Uvedený kód implementuje balík `spinlock`. V tomto kóde sa `SpinLock` implementuje pomocou balíka `sync/atomic`. Metóda `Lock` sa pokúša získať zámok pomocou `atomic.CompareAndSwapUintptr` a metóda `Unlock` zámok uvoľní pomocou `atomic.StoreUintptr`. Keďže sa tento spôsob bez prestávky opakovane pokúša získať zámok, až dokým ho nezíska, neustále spotrebúva CPU a môže uviaznuť v nekonečnej slučke. Preto sa `spinlock` odporúča používať iba na jednoduchú synchronizáciu alebo v prípadoch, keď sa čaká len krátko.

#### sync.Mutex

`mutex` je nástroj na synchronizáciu medzi goroutine. `mutex` poskytovaný balíkom `sync` ponúka metódy `Lock`, `Unlock`, `RLock`, `RUnlock` a ďalšie. `mutex` možno vytvoriť pomocou `sync.Mutex` a na zámok na čítanie/zápis možno použiť `sync.RWMutex`.

```go
package main

import (
    "sync"
)

func main() {
    var mu sync.Mutex
    var count int

    go func() {
        mu.Lock()
        count++
        mu.Unlock()
    }()

    mu.Lock()
    count++
    mu.Unlock()

    println(count)
}
```

V uvedenom kóde sa dve goroutine takmer súčasne dostanú k rovnakej premennej `count`. Ak sa kód, ktorý pristupuje k premennej `count`, spraví kritickou sekciou pomocou `mutex`, zabráni sa súbežnému prístupu k premennej `count`. Potom tento kód vypíše `2` bez ohľadu na to, koľkokrát sa spustí.

#### sync.RWMutex

`sync.RWMutex` je `mutex`, ktorý umožňuje rozlišovať zámok na čítanie a zámok na zápis. Zámok na čítanie možno nastaviť a uvoľniť pomocou metód `RLock` a `RUnlock`.

```go
package cmap

import (
    "sync"
)

type ConcurrentMap[K comparable, V any] struct {
    sync.RWMutex
    data map[K]V
}

func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
    m.RLock()
    defer m.RUnlock()

    value, ok := m.data[key]
    return value, ok
}

func (m *ConcurrentMap[K, V]) Set(key K, value V) {
    m.Lock()
    defer m.Unlock()

    m.data[key] = value
}
```

Uvedený kód implementuje `ConcurrentMap` pomocou `sync.RWMutex`. V tomto kóde metóda `Get` nastavuje zámok na čítanie a metóda `Set` nastavuje zámok na zápis, vďaka čomu možno bezpečne pristupovať k mape `data` a upravovať ju. Zámok na čítanie je potrebný preto, aby sa v prípadoch, keď je čítanie časté, nenastavoval zámok na zápis, ale iba zámok na čítanie, a aby mohlo viacero goroutine vykonávať čítanie súčasne. Vďaka tomu možno zvýšiť výkon v prípadoch, keď nedochádza k zmene stavu a zámok na zápis nie je potrebný.

#### fakelock

`fakelock` je jednoduchý trik, ktorý implementuje `sync.Locker`. Táto štruktúra poskytuje rovnaké metódy ako `sync.Mutex`, ale v skutočnosti nevykonáva žiadnu činnosť.

```go
package fakelock

type FakeLock struct{}

func (f *FakeLock) Lock() {}

func (f *FakeLock) Unlock() {}
```

Uvedený kód implementuje balík `fakelock`. Tento balík implementuje `sync.Locker` a poskytuje metódy `Lock` a `Unlock`, no v skutočnosti nevykonáva žiadnu činnosť. Prečo je taký kód potrebný, vysvetlím pri vhodnej príležitosti.

### waitgroup

#### sync.WaitGroup

`sync.WaitGroup` je nástroj, ktorý čaká, kým sa dokončia všetky úlohy goroutine. Poskytuje metódy `Add`, `Done` a `Wait`: metódou `Add` sa pridáva počet goroutine, metódou `Done` sa signalizuje, že úloha goroutine skončila, a metódou `Wait` sa čaká, kým sa dokončia úlohy všetkých goroutine.

```go
package main

import (
    "sync"
    "sync/atomic"
)

func main() {
    wg := sync.WaitGroup{}
    c := atomic.Int64{}

    for i := 0; i < 100 ; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Add(1)
        }()
    }

    wg.Wait()
    println(c.Load())
}
```

Uvedený kód pomocou `sync.WaitGroup` pripočítava hodnotu do premennej `c` súbežne zo 100 goroutine. V tomto kóde sa pomocou `sync.WaitGroup` počká, kým skončia všetky goroutine, a potom sa vypíše hodnota pripočítaná do premennej `c`. Pri jednoduchom `fork & join` niekoľkých úloh stačí použiť iba kanály, no pri `fork & join` veľkého množstva úloh je dobrou voľbou aj `sync.WaitGroup`.

#### with slice

Ak sa použije spolu so slice, `waitgroup` sa môže stať výborným nástrojom na správu súbežne vykonávaných úloh bez zámkov.

```go
package main

import (
	"fmt"
	"sync"
    "rand"
)

func main() {
	var wg sync.WaitGroup
	arr := [10]int{}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			arr[id] = rand.Intn(100)
		}(i)
	}

	wg.Wait()
	fmt.Println("Done")

    for i, v := range arr {
        fmt.Printf("arr[%d] = %d\n", i, v)
    }
}
```

Uvedený kód používa iba `waitgroup`, pričom každá goroutine súbežne vygeneruje 10 náhodných celých čísel a uloží ich na pridelený index. V tomto kóde sa pomocou `waitgroup` počká, kým skončia všetky goroutine, a potom sa vypíše `Done`. Týmto spôsobom možno pomocou `waitgroup` nechať viacero goroutine vykonávať úlohy súbežne, dáta ukladať bez zámkov, kým neskončia všetky goroutine, a po skončení úloh ich spracovať naraz.

#### golang.org/x/sync/errgroup.ErrGroup

`errgroup` je balík, ktorý rozširuje `sync.WaitGroup`. Na rozdiel od `sync.WaitGroup` `errgroup` pri vzniku chyby v čo i len jednej z úloh goroutine zruší všetky goroutine a vráti chybu.

```go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
)

func main() {
    g, ctx := errgroup.WithContext(context.Background())
    _ = ctx

    for i := 0; i < 10; i++ {
        i := i
        g.Go(func() error {
            if i == 5 {
                return fmt.Errorf("error")
            }
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Println(err)
    }
}
```

Uvedený kód pomocou `errgroup` vytvára 10 goroutine a v piatej goroutine spôsobuje chybu. Chybu v piatej goroutine som vyvolal úmyselne, aby som ukázal prípad, keď nastane chyba. Pri skutočnom používaní sa `errgroup` použije tak, že sa vytvoria goroutine a v každej goroutine sa pri vzniku chyby vykonajú rôzne následné spracovania.

### once

Je to nástroj na jednorazové vykonanie kódu, ktorý sa má spustiť iba raz. Súvisiaci kód možno spustiť pomocou nasledujúcich konštruktorov.

```go
func OnceFunc(f func()) func()
func OnceValue[T any](f func() T) func() T
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
```

#### OnceFunc

`OnceFunc` jednoducho zabezpečí, že sa daná funkcia počas celého behu vykoná presne raz.

```go
package main

import "sync"

func main() {
    once := sync.OnceFunc(func() {
        println("Hello, World!")
    })

    once()
    once()
    once()
    once()
    once()
}
```

Uvedený kód pomocou `sync.OnceFunc` vypíše `Hello, World!`. V tomto kóde sa pomocou `sync.OnceFunc` vytvorí funkcia `once`, a aj keď sa funkcia `once` zavolá viackrát, `Hello, World!` sa vypíše iba raz.

#### OnceValue

`OnceValue` nezabezpečuje iba to, že sa daná funkcia vykoná presne raz počas celého behu, ale uloží aj jej návratovú hodnotu a pri ďalšom volaní vráti uloženú hodnotu.

```go
package main

import "sync"

func main() {
    c := 0
    once := sync.OnceValue(func() int {
        c += 1
        return c
    })

    println(once())
    println(once())
    println(once())
    println(once())
    println(once())
}
```

Uvedený kód pomocou `sync.OnceValue` zvyšuje premennú `c` o 1. V tomto kóde sa pomocou `sync.OnceValue` vytvorí funkcia `once`, a aj keď sa funkcia `once` zavolá viackrát, premenná `c` sa zvýši iba raz a vráti sa hodnota 1.

#### OnceValues

`OnceValues` funguje rovnako ako `OnceValue`, ale môže vrátiť viacero hodnôt.

```go
package main

import "sync"

func main() {
    c := 0
    once := sync.OnceValues(func() (int, int) {
        c += 1
        return c, c
    })

    a, b := once()
    println(a, b)
    a, b = once()
    println(a, b)
    a, b = once()
    println(a, b)
    a, b = once()
    println(a, b)
    a, b = once()
    println(a, b)
}
```

Uvedený kód pomocou `sync.OnceValues` zvyšuje premennú `c` o 1. V tomto kóde sa pomocou `sync.OnceValues` vytvorí funkcia `once`, a aj keď sa funkcia `once` zavolá viackrát, premenná `c` sa zvýši iba raz a vráti sa hodnota 1.

### atomic

Balík `atomic` poskytuje atomické operácie. Balík `atomic` ponúka metódy ako `Add`, `CompareAndSwap`, `Load`, `Store` a `Swap`, no v poslednej dobe sa odporúča používať typy ako `Int64`, `Uint64` a `Pointer`.

```go
package main

import (
    "sync"
    "sync/atomic"
)

func main() {
    wg := sync.WaitGroup{}
    c := atomic.Int64{}

    for i := 0; i < 100 ; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Add(1)
        }()
    }

    wg.Wait()
    println(c.Load())
}
```

Ide o príklad, ktorý sme použili skôr. Uvedený kód pomocou typu `atomic.Int64` atomicky zvyšuje premennú `c`. Metódami `Add` a `Load` možno premennú atomicky zvýšiť a načítať. Ďalej možno metódou `Store` uložiť hodnotu, metódou `Swap` hodnotu vymeniť a metódou `CompareAndSwap` porovnať hodnotu a, ak vyhovuje, vymeniť ju.

### cond

#### sync.Cond

Balík `cond` poskytuje podmienkové premenné. Balík `cond` možno vytvoriť pomocou `sync.Cond` a poskytuje metódy `Wait`, `Signal` a `Broadcast`.

```go
package main

import (
    "sync"
)

func main() {
    c := sync.NewCond(&sync.Mutex{})
    ready := false

    go func() {
        c.L.Lock()
        ready = true
        c.Signal()
        c.L.Unlock()
    }()

    c.L.Lock()
    for !ready {
        c.Wait()
    }
    c.L.Unlock()

    println("Ready!")
}
```

Uvedený kód pomocou `sync.Cond` čaká, kým premenná `ready` nadobudne hodnotu `true`. V tomto kóde sa pomocou `sync.Cond` počká, kým premenná `ready` nadobudne hodnotu `true`, a potom sa vypíše `Ready!`. Týmto spôsobom možno pomocou `sync.Cond` nechať viacero goroutine súbežne čakať, kým sa nesplní určitá podmienka.

Využitím tohto možno implementovať jednoduchú `queue`.

```go
package queue

import (
    "sync"
    "sync/atomic"
)

type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type Queue[T any] struct {
    sync.Mutex
    Cond *sync.Cond
    Head *Node[T]
    Tail *Node[T]
    Len  int
}

func New[T any]() *Queue[T] {
    q := &Queue[T]{}
    q.Cond = sync.NewCond(&q.Mutex)
    return q
}

func (q *Queue[T]) Push(value T) {
    q.Lock()
    defer q.Unlock()

    node := &Node[T]{Value: value}
    if q.Len == 0 {
        q.Head = node
        q.Tail = node
    } else {
        q.Tail.Next = node
        q.Tail = node
    }
    q.Len++
    q.Cond.Signal()
}

func (q *Queue[T]) Pop() T {
    q.Lock()
    defer q.Unlock()

    for q.Len == 0 {
        q.Cond.Wait()
    }

    node := q.Head
    q.Head = q.Head.Next
    q.Len--
    return node.Value
}
```

Ak sa takto použije `sync.Cond`, namiesto toho, aby sa spotrebúvalo veľa CPU pomocou `spin-lock`, možno efektívne čakať a po splnení podmienky znovu pokračovať v činnosti.

### semaphore

#### golang.org/x/sync/semaphore.Semaphore

Balík `semaphore` poskytuje semafór. Balík `semaphore` možno vytvoriť pomocou `golang.org/x/sync/semaphore.Semaphore` a poskytuje metódy `Acquire`, `Release` a `TryAcquire`.

```go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/semaphore"
)

func main() {
    s := semaphore.NewWeighted(1)

    if s.TryAcquire(1) {
        fmt.Println("Acquired!")
    } else {
        fmt.Println("Not Acquired!")
    }

    s.Release(1)
}
```

Uvedený kód vytvára semafór pomocou `semaphore`, získava ho metódou `Acquire` a uvoľňuje metódou `Release`. V tomto kóde sa ukázal spôsob získania a uvoľnenia semafóra pomocou `semaphore`.

## Záver

Základný obsah by mal stačiť. Na základe tohto článku dúfam, že pochopíte, ako pomocou goroutine spravovať súbežnosť, a budete ju vedieť používať v praxi. Dúfam, že tento článok vám bude užitočný. Ďakujem.
