---
id: 01933e13f963942e4c9bc8c3c89fbc2b
author: snowmerak
title: Go-samtidighetsstarterpakke
description: Vi presenterer en rekke verktøy og teknikker for samtidighetsstyring i Go, blant annet goroutiner, kanaler og mutexer.
language: "no"
date: 2024-10-18T05:51:30.010680581Z
path: /blog/posts/go-concurrency-starter-pack-z221a3399
category: concurrency
---

## Oversikt

### Kort introduksjon

Go-språket har mange verktøy for håndtering av samtidighet. I denne artikkelen vil jeg presentere noen av dem sammen med noen triks.

### Goroutiner?

goroutine er en ny form for samtidighetsmodell som støttes av Go-språket. Vanligvis får et program OS-tråder fra operativsystemet for å utføre flere oppgaver samtidig, og utfører oppgaver parallelt tilsvarende antall kjerner. For å oppnå samtidighet på et mindre nivå opprettes grønne tråder i brukerrommet, slik at flere grønne tråder kjører oppgaver etter tur innenfor én OS-tråd. Goroutiner er imidlertid laget som en mer kompakt og effektiv variant av slike grønne tråder. Goroutiner bruker mindre minne enn tråder og kan opprettes og byttes raskere enn tråder.

For å bruke goroutiner er det tilstrekkelig å bruke nøkkelordet `go`. Dette gjør det intuitivt mulig å kjøre synkron kode som asynkron kode under programmering.

```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 {}
}
```

Denne koden endrer en synkron kode, som venter ett sekund og deretter skriver ut `Hello, World!`, til en asynkron flyt. Selv om eksemplet nå er enkelt, blir lesbarhet, synlighet og forståelighet bedre enn med metoder som async await eller promise når man konverterer en mer kompleks kode fra synkron til asynkron.

I mange tilfeller kan dårlig goroutine-kode dog oppstå når man ikke forstår flyten der synkron kode bare kalles asynkront, eller flyter som `fork & join` (som ligner på divide and conquer). For å håndtere slike tilfeller vil jeg i denne artikkelen presentere noen metoder og teknikker.

## Håndtering av samtidighet

### context

Det kan være overraskende at `context` dukker opp som den første håndteringsteknikken. Men i Go-språket spiller `context` en fremragende rolle i å håndtere hele arbeidstreet, utover en enkel avbrytelsesfunksjon. For dem som ikke kjenner til det, vil jeg kort forklare pakken.

```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)
}
```

Koden ovenfor bruker `context` til å skrive ut `Context is done!` etter ett sekund. `context` kan sjekke om avbrytelse har skjedd via metoden `Done()`, og tilbyr ulike avbrytelsesmetoder via metoder som `WithCancel`, `WithTimeout`, `WithDeadline` og `WithValue`.

La oss lage et enkelt eksempel. Anta at du skriver kode som bruker `aggregator`-mønsteret for å hente `user`, `post` og `comment`, og at alle forespørsler må fullføres innen to sekunder. Da kan du skrive følgende.

```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!")
    }
}
```

Koden ovenfor skriver ut `Timeout!` hvis ikke alle data hentes innen to sekunder, og skriver ut `All data is fetched!` hvis alle data hentes. På denne måten kan du enkelt håndtere avbrytelse og tidsavbrudd i kode der flere goroutiner kjører, når du bruker `context`.

Ulike funksjoner og metoder knyttet til context finnes på [godoc context](https://pkg.go.dev/context). Jeg håper at du kan lære det enkle og bruke det med letthet.

### channel

#### unbuffered channel

`channel` er et verktøy for kommunikasjon mellom goroutiner. En `channel` kan opprettes med `make(chan T)`, der `T` er datatypen som kanalen skal overføre. Data sendes og mottas med `<-`, og `channel` lukkes med `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)
    }
}
```

Koden ovenfor bruker `channel` til å skrive ut 1 og 2. Dette eksemplet viser bare enkel sending og mottak av verdier i en kanal, men `channel` tilbyr flere funksjoner enn dette. Først skal vi se på `buffered channel` og `unbuffered channel`. Før vi begynner, må det nevnes at eksemplet ovenfor bruker en `unbuffered channel`, der handlingen med å sende data til kanalen og handlingen med å motta data må skje samtidig. Hvis disse handlingene ikke skjer samtidig, kan en deadlock oppstå.

#### buffered channel

Hva om koden ovenfor ikke bare skriver ut, men utfører to prosesser med tung oppgave? Hvis den andre prosessen leser og behandler data, og bruker lang tid på dette, vil også den første prosessen stoppe i samme tidsrom. For å unngå denne situasjonen kan vi bruke `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)
    }
}
```

Koden ovenfor bruker `buffered channel` til å skrive ut 1 og 2. I denne koden trenger ikke handlingen med å sende data til `channel` og handlingen med å motta data å skje samtidig, fordi vi har laget en buffer. Når kanalen har en buffer, får den kapasitet tilsvarende bufferlengden, og dette kan forhindre forsinkelser i arbeid som skyldes påvirkning fra oppgaver med lavere prioritet.

#### select

Når du arbeider med flere kanaler, kan du enkelt implementere en `fan-in`-struktur ved hjelp av `select`-syntaksen.

```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)
        }
    }
}
```

Koden ovenfor oppretter tre kanaler som periodisk sender 1, 2 og 3, og bruker `select` til å motta og skrive ut verdier fra kanalene. På denne måten kan `select` motta data samtidig fra flere kanaler og behandle verdiene i den rekkefølgen de kommer.

#### for range

`channel` kan enkelt motta data ved hjelp av `for range`. Når `for range` brukes på en kanal, kjøres løkken hver gang det legges til data i kanalen, og løkken avsluttes når kanalen lukkes.

```go
package main

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

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

Koden ovenfor bruker `channel` til å skrive ut 1 og 2. I denne koden bruker vi `for range` til å motta og skrive ut data hver gang det legges til data i kanalen. Når kanalen lukkes, avsluttes løkken.

Som vist flere ganger tidligere kan denne syntaksen også brukes som et enkelt synkroniseringsverktøy.

```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 {}
}
```

Koden ovenfor venter ett sekund og skriver deretter ut `Hello, World!`. I denne koden brukes `channel` til å endre synkron kode til asynkron kode. På denne måten kan du enkelt endre synkron kode til asynkron kode med `channel` og angi et `join`-punkt.

#### etc

1. Hvis du sender data til eller mottar data fra en nil channel, kan det oppstå en uendelig løkke som fører til deadlock.
2. Hvis du sender data etter at en kanal er lukket, oppstår en panic.
3. Selv om du ikke lukker en kanal eksplisitt, vil GC samle den inn og lukke den.

### mutex

#### spinlock

`spinlock` er en synkroniseringsmetode som gjentatte ganger prøver å ta låsen i en løkke. I Go-språket kan du enkelt prøve å implementere en spinlock ved hjelp av pekere.

```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{}
}
```

Koden ovenfor implementerer pakken `spinlock`. Her er `SpinLock` implementert ved hjelp av pakken `sync/atomic`. I metoden `Lock` brukes `atomic.CompareAndSwapUintptr` til å forsøke å ta låsen, og i metoden `Unlock` brukes `atomic.StoreUintptr` til å frigjøre låsen. Siden denne metoden prøver å ta låsen uten pause, bruker den CPU kontinuerlig til låsen er tatt, og kan dermed henge seg i en uendelig løkke. Derfor bør `spinlock` brukes til enkel synkronisering eller bare i korte tidsrom.

#### sync.Mutex

`mutex` er et verktøy for synkronisering mellom goroutiner. `mutex` fra pakken `sync` tilbyr metodene `Lock`, `Unlock`, `RLock` og `RUnlock`. En `mutex` kan opprettes med `sync.Mutex`, og du kan også bruke en lese-/skrivelås med `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)
}
```

I koden ovenfor får to goroutiner nesten samtidig tilgang til den samme variabelen `count`. Hvis du bruker `mutex` for å gjøre koden som får tilgang til `count` til en kritisk seksjon, kan du hindre samtidig tilgang til `count`. Da vil denne koden alltid skrive ut `2`, uansett hvor mange ganger den kjøres.

#### sync.RWMutex

`sync.RWMutex` er en `mutex` der lese- og skrivelås kan brukes separat. Du kan ta og frigjøre leselåsen med metodene `RLock` og `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
}
```

Koden ovenfor implementerer `ConcurrentMap` ved hjelp av `sync.RWMutex`. I denne koden tar metoden `Get` leselåsen, og metoden `Set` tar skrivelåsen, slik at kartet `data` kan nås og endres trygt. Grunnen til at leselås er nødvendig, er at når det er mange rene leseoperasjoner, kan flere goroutiner utføre lesing samtidig ved bare å ta leselåsen, uten skrivelås. På denne måten kan ytelsen forbedres ved å bare ta leselåsen i tilfeller der det ikke er nødvendig å endre tilstanden og skrivelås derfor ikke trengs.

#### fakelock

`fakelock` er et enkelt triks som implementerer `sync.Locker`. Denne strukturen tilbyr de samme metodene som `sync.Mutex`, men utfører ingen faktisk handling.

```go
package fakelock

type FakeLock struct{}

func (f *FakeLock) Lock() {}

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

Koden ovenfor implementerer pakken `fakelock`. Pakken implementerer `sync.Locker` og tilbyr metodene `Lock` og `Unlock`, men den gjør faktisk ingenting. Hvorfor en slik kode er nødvendig, vil jeg forklare ved en senere anledning.

### waitgroup

#### sync.WaitGroup

`sync.WaitGroup` er et verktøy som venter til alle oppgavene til goroutinene er fullført. Den tilbyr metodene `Add`, `Done` og `Wait`. Med metoden `Add` legger du til antall goroutiner, med `Done` melder du at en goroutine er ferdig, og med `Wait` venter du til alle goroutinene er ferdige.

```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())
}
```

Koden ovenfor bruker `sync.WaitGroup` til å la 100 goroutiner samtidig legge til verdier i variabelen `c`. I denne koden venter `sync.WaitGroup` til alle goroutinene er ferdige, og deretter skrives verdien ut som er lagt til i `c`. Når du bare skal utføre `fork & join` på noen få oppgaver, er det tilstrekkelig å bruke kanaler alene, men når du skal utføre `fork & join` på et stort antall oppgaver, er `sync.WaitGroup` også et godt valg.

#### with slice

Når den brukes sammen med en slice, kan `waitgroup` bli et utmerket verktøy for å håndtere samtidig utførelse uten lås.

```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)
    }
}
```

Koden ovenfor bruker kun `waitgroup`, der hver goroutine samtidig genererer ett av 10 tilfeldige heltall og lagrer det på den tildelte indeksen. I denne koden venter `waitgroup` til alle goroutinene er ferdige, og deretter skrives `Done` ut. På denne måten kan flere goroutiner utføre oppgaver samtidig, lagre data uten lås mens alle goroutinene kjører, og utføre etterbehandling samlet etter at alle oppgaver er avsluttet.

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

`errgroup` er en pakke som utvider `sync.WaitGroup`. I motsetning til `sync.WaitGroup` avbryter `errgroup` alle goroutiner og returnerer feilen hvis en feil oppstår i en av goroutinene.

```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)
    }
}
```

Koden ovenfor bruker `errgroup` til å opprette 10 goroutiner, der den femte goroutinen forårsaker en feil. Jeg har med vilje laget en feil i den femte goroutinen for å vise et tilfelle der feil oppstår. I praksis brukes `errgroup` til å opprette goroutiner, og deretter kan man utføre ulik etterbehandling for hver goroutine der det oppstår en feil.

### once

Dette er et verktøy for å kjøre kode som bare skal kjøres én gang. Du kan kjøre relatert kode via konstruktørene nedenfor.

```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` sikrer ganske enkelt at den aktuelle funksjonen bare kjøres én gang i hele programmet.

```go
package main

import "sync"

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

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

Koden ovenfor bruker `sync.OnceFunc` til å skrive ut `Hello, World!`. I denne koden opprettes funksjonen `once` med `sync.OnceFunc`, og selv om `once` kalles flere ganger, skrives `Hello, World!` bare ut én gang.

#### OnceValue

`OnceValue` sikrer ikke bare at funksjonen kjøres én gang i hele programmet, men lagrer også returverdien til funksjonen, og returnerer den lagrede verdien ved nye kall.

```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())
}
```

Koden ovenfor bruker `sync.OnceValue` til å øke variabelen `c` med 1. I denne koden opprettes funksjonen `once` med `sync.OnceValue`, og selv om `once` kalles flere ganger, økes variabelen `c` bare én gang, og returverdien er 1.

#### OnceValues

`OnceValues` fungerer på samme måte som `OnceValue`, men kan returnere flere verdier.

```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)
}
```

Koden ovenfor bruker `sync.OnceValues` til å øke variabelen `c` med 1. I denne koden opprettes funksjonen `once` med `sync.OnceValues`, og selv om `once` kalles flere ganger, økes variabelen `c` bare én gang, og returverdien er 1.

### atomic

Pakken `atomic` tilbyr atomiske operasjoner. Den tilbyr metodene `Add`, `CompareAndSwap`, `Load`, `Store` og `Swap`, men i nyere tid anbefales bruk av typer som `Int64`, `Uint64` og `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())
}
```

Dette er eksemplet som ble brukt tidligere. Koden bruker typen `atomic.Int64` til å øke variabelen `c` atomisk. Med metodene `Add` og `Load` kan variabelen økes atomisk og leses. I tillegg kan man lagre en verdi med metoden `Store`, bytte verdi med metoden `Swap`, og med metoden `CompareAndSwap` sammenligne verdien og bytte den hvis den passer.

### cond

#### sync.Cond

Pakken `cond` tilbyr betingelsesvariabler. Den kan opprettes med `sync.Cond` og tilbyr metodene `Wait`, `Signal` og `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!")
}
```

Koden ovenfor bruker `sync.Cond` til å vente til variabelen `ready` blir `true`. I denne koden venter `sync.Cond` til variabelen `ready` blir `true`, og deretter skrives `Ready!` ut. På denne måten kan `sync.Cond` la flere goroutiner vente samtidig til en bestemt betingelse er oppfylt.

Ved å bruke dette kan du implementere en enkel `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
}
```

Når du bruker `sync.Cond` på denne måten, kan du vente effektivt i stedet for å bruke mye CPU med `spin-lock`, og fortsette å kjøre når betingelsen er oppfylt.

### semaphore

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

Pakken `semaphore` tilbyr semaforer. Den kan opprettes med `golang.org/x/sync/semaphore.Semaphore` og tilbyr metodene `Acquire`, `Release` og `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)
}
```

Koden ovenfor oppretter en semafor med `semaphore`, henter den med metoden `Acquire` og frigjør den med metoden `Release`. I denne koden vises hvordan man henter og frigjør en semafor med `semaphore`.

## Avslutning

Det grunnleggende innholdet burde være tilstrekkelig her. Jeg håper at du basert på innholdet i denne artikkelen kan forstå hvordan man håndterer samtidighet med goroutiner, og at du kan bruke det i praksis. Jeg håper også at denne artikkelen har vært nyttig for deg. Takk.
