---
id: 01933e13f963942e4c9bc8c3c89fbc2b
author: snowmerak
title: Go-samtidighedsstarterpakke
description: Vi præsenterer en række værktøjer og teknikker til styring af samtidighed i Go, herunder goroutines, kanaler og mutexes.
language: da
date: 2024-10-18T05:51:30.010680581Z
path: /blog/posts/go-concurrency-starter-pack-z221a3399
category: concurrency
---

## Oversigt

### Kort introduktion

Go-sproget har mange værktøjer til håndtering af samtidighed. I denne artikel vil jeg præsentere nogle af dem samt nogle tricks.

### Goroutines?

goroutine er en ny form for samtidighedsmodel, der understøttes af Go-sproget. Normalt modtager et program OS-tråde fra operativsystemet for at udføre flere opgaver samtidigt, og udfører opgaver parallelt svarende til antallet af kerner. For at udføre samtidighed i mindre skala oprettes grønne tråde i user space, således at flere grønne tråde kører skiftevis inden for én OS-tråd. Goroutines er dog gjort endnu mindre og mere effektive end sådanne grønne tråde. Disse goroutines bruger mindre hukommelse end tråde og kan oprettes og skiftes hurtigere end tråde.

For at bruge goroutines er det tilstrækkeligt blot at anvende nøgleordet `go`. Dette gør det muligt intuitivt at kø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 kode ændrer en synkron kode, der holder pause i 1 sekund og derefter udskriver `Hello, World!`, til et asynkront flow. Selvom dette eksempel er simpelt, bliver læsbarhed, synlighed og forståelighed bedre, når man ændrer mere komplekse kodestykker fra synkron til asynkron kode, end med eksisterende metoder som async await eller promise.

Dog opstår der i mange tilfælde dårlig goroutine-kode, hvis man ikke forstår flowet ved blot at kalde synkron kode asynkront, og flows som `fork & join` (som ligner divide and conquer). For at forberede sig på sådanne tilfælde vil jeg i denne artikel præsentere nogle metoder og teknikker.

## Håndtering af samtidighed

### context

Det kan være overraskende, at `context` optræder som den første håndteringsteknik. Men i Go-sproget spiller `context` en fremragende rolle i at administrere hele opgavetræet ud over en simpel annulleringsfunktion. For dem, der ikke kender pakken, vil jeg kort forklare den.

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

Ovenstående kode bruger `context` til at udskrive `Context is done!` efter 1 sekund. Man kan kontrollere, om der er annulleret, via metoden `Done()` på `context`, og den tilbyder forskellige annulleringsmetoder gennem metoder som `WithCancel`, `WithTimeout`, `WithDeadline` og `WithValue`.

Lad os lave et simpelt eksempel. Antag, at man for at hente data skriver kode, der bruger `aggregator`-mønsteret til at hente `user`, `post` og `comment`, og at alle anmodninger skal være gennemført inden for 2 sekunder. Det kan så skrives som følger.

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

Ovenstående kode udskriver `Timeout!`, hvis alle data ikke er hentet inden for 2 sekunder, og `All data is fetched!`, hvis alle data hentes. På denne måde kan man med `context` nemt håndtere annullering og timeout, selv i kode hvor flere goroutines kører.

Forskellige context-relaterede funktioner og metoder kan findes på [godoc context](https://pkg.go.dev/context). Jeg håber, at du lærer det grundlæggende og kan bruge det med lethed.

### channel

#### unbuffered channel

`channel` er et værktøj til kommunikation mellem goroutines. En `channel` kan oprettes med `make(chan T)`. Her er `T` den datatype, som kanalen skal overføre. Man kan sende og modtage data med `<-`, og lukke en `channel` 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)
    }
}
```

Ovenstående kode bruger `channel` til at udskrive 1 og 2. Denne kode viser blot at sende og modtage værdier via en kanal. Men `channel` tilbyder flere funktioner end dette. Lad os først se på `buffered channel` og `unbuffered channel`. Inden vi begynder, skal det bemærkes, at eksemplet ovenfor er en `unbuffered channel`, hvor handlingen med at sende data til kanalen og handlingen med at modtage data skal ske samtidigt. Hvis disse handlinger ikke sker samtidigt, kan der opstå deadlock.

#### buffered channel

Hvad hvis den ovenstående kode ikke blot udskriver, men er to processer, der udfører tunge opgaver? Hvis den anden proces læser og behandler, men bruger lang tid, vil den første proces også stoppe i så lang tid. For at forhindre denne situation kan vi bruge `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)
    }
}
```

Ovenstående kode bruger `buffered channel` til at udskrive 1 og 2. I denne kode bruges `buffered channel`, så handlingen med at sende data til kanalen og handlingen med at modtage data ikke behøver at ske samtidigt. Når der er en buffer i kanalen, opstår der plads svarende til bufferlængden, og dermed kan forsinkelser forårsaget af påvirkning fra efterfølgende opgaver forhindres.

#### select

Når man arbejder med flere kanaler, kan man let implementere en `fan-in`-struktur ved hjælp af `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)
        }
    }
}
```

Ovenstående kode opretter tre kanaler, der periodisk sender 1, 2 og 3, og bruger `select` til at modtage værdier fra kanalerne og udskrive dem. På denne måde kan man med `select` modtage data fra flere kanaler samtidigt og behandle dem i takt med, at værdierne modtages.

#### for range

`channel` kan nemt modtage data med `for range`. Når `for range` bruges på en kanal, udføres loopet hver gang der tilføjes data til kanalen, og loopet afsluttes, 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)
    }
}
```

Ovenstående kode bruger `channel` til at udskrive 1 og 2. I denne kode bruges `for range` til at modtage og udskrive data, hver gang der tilføjes data til kanalen. Når kanalen lukkes, afsluttes loopet.

Som tidligere vist kan denne syntaks også bruges som et simpelt synkroniseringsmiddel.

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

Ovenstående kode holder pause i 1 sekund og udskriver derefter `Hello, World!`. I denne kode bruges `channel` til at ændre synkron kode til asynkron kode. På denne måde kan man med `channel` nemt ændre synkron kode til asynkron kode og etablere et `join`-punkt.

#### etc

1. Hvis man sender eller modtager data på en nil channel, kan der opstå en uendelig løkke og deadlock.
2. Hvis man sender data efter at have lukket en kanal, opstår der en panic.
3. Selv hvis man ikke lukker en kanal eksplicit, lukker GC den, når den indsamler den.

### mutex

#### spinlock

`spinlock` er en synkroniseringsmetode, der gentagne gange i en løkke forsøger at tage låsen. I Go-sproget kan man let prøve at implementere en spinlock ved hjælp af pointere.

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

Ovenstående kode implementerer pakken `spinlock`. Denne kode implementerer `SpinLock` ved hjælp af pakken `sync/atomic`. I metoden `Lock` forsøges låsen med `atomic.CompareAndSwapUintptr`, og i metoden `Unlock` frigives låsen med `atomic.StoreUintptr`. Da denne metode forsøger at tage låsen uden pause, bruger den CPU kontinuerligt, indtil låsen opnås, og den kan ende i en uendelig løkke. Derfor er det bedst at bruge `spinlock` til simpel synkronisering eller til tilfælde, hvor den kun bruges i kort tid.

#### sync.Mutex

`mutex` er et værktøj til synkronisering mellem goroutines. `mutex`, som leveres af pakken `sync`, tilbyder metoder som `Lock`, `Unlock`, `RLock` og `RUnlock`. En `mutex` kan oprettes med `sync.Mutex`, og man kan også bruge en læse/skrive-lå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 ovenstående kode får to goroutines næsten samtidigt adgang til den samme variabel `count`. Hvis man gør koden, der tilgår variablen `count`, til et kritisk område ved hjælp af `mutex`, kan samtidig adgang til `count` forhindres. Så vil denne kode uanset antal kørsler altid udskrive `2`.

#### sync.RWMutex

`sync.RWMutex` er en `mutex`, hvor læselåse og skrivelåse kan bruges adskilt. Man kan tage og frigive læselåsen med metoderne `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
}
```

Ovenstående kode implementerer `ConcurrentMap` ved hjælp af `sync.RWMutex`. I denne kode tager metoden `Get` en læselås, og metoden `Set` tager en skrivelås, så `data`-mappet kan tilgås og ændres sikkert. Læselåsen er nødvendig, fordi man i tilfælde med mange simple læseoperationer ikke tager en skrivelås, men kun en læselås, så flere goroutines kan udføre læseoperationer samtidigt. Dermed kan man forbedre ydeevnen ved kun at tage en læselås i de tilfælde, hvor der ikke er behov for en skrivelås, fordi tilstanden ikke ændres.

#### fakelock

`fakelock` er et simpelt trick, der implementerer `sync.Locker`. Denne struct tilbyder de samme metoder som `sync.Mutex`, men udfører faktisk ingen handling.

```go
package fakelock

type FakeLock struct{}

func (f *FakeLock) Lock() {}

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

Ovenstående kode implementerer pakken `fakelock`. Pakken implementerer `sync.Locker` og tilbyder metoderne `Lock` og `Unlock`, men gør faktisk ingenting. Jeg vil forklare, hvorfor denne kode er nødvendig, når der er mulighed for det.

### waitgroup

#### sync.WaitGroup

`sync.WaitGroup` er et værktøj, der venter, indtil alle goroutines opgaver er afsluttet. Den tilbyder metoderne `Add`, `Done` og `Wait`. Med metoden `Add` tilføjes antallet af goroutines, med metoden `Done` meddeles det, at en goroutines opgave er afsluttet, og med metoden `Wait` venter man, indtil alle goroutines opgaver er afsluttet.

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

Ovenstående kode bruger `sync.WaitGroup` til at lade 100 goroutines samtidigt lægge værdi til variablen `c`. I denne kode venter `sync.WaitGroup` på, at alle goroutines er færdige, og derefter udskrives den værdi, der er lagt til `c`. Når man blot laver `fork & join` af nogle få opgaver, er det tilstrækkeligt kun at bruge kanaler, men ved `fork & join` af mange opgaver er `sync.WaitGroup` også et godt valg.

#### with slice

Når det bruges sammen med slices, kan `waitgroup` blive et fremragende værktøj til at håndtere samtidig udførelse uden 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)
    }
}
```

Ovenstående kode bruger udelukkende `waitgroup`, hvor hver goroutine samtidigt genererer et tilfældigt heltal mellem 0 og 99 og gemmer det på det tildelte indeks. I denne kode venter `waitgroup` på, at alle goroutines er færdige, og udskriver derefter `Done`. På denne måde kan man med `waitgroup` lade flere goroutines udføre opgaver samtidigt, gemme data uden lås, indtil alle goroutines er færdige, og derefter udføre efterbehandling samlet.

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

`errgroup` er en pakke, der udvider `sync.WaitGroup`. I modsætning til `sync.WaitGroup` annullerer `errgroup` alle goroutines og returnerer fejlen, hvis en fejl opstår i en af goroutines opgaver.

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

Ovenstående kode bruger `errgroup` til at oprette 10 goroutines og får den 5. goroutine til at give en fejl. Jeg har med vilje fremkaldt en fejl i den femte goroutine for at vise tilfældet, hvor der opstår en fejl. I praksis bruger man `errgroup` til at oprette goroutines og udføre forskellig efterbehandling i tilfælde, hvor der opstår fejl i de enkelte goroutines.

### once

Det er et værktøj til at udføre kode, der kun må køres én gang. Man kan køre den relaterede kode gennem følgende konstruktører.

```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` sørger blot for, at den pågældende funktion kun kan køres præcis én gang i hele programmet.

```go
package main

import "sync"

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

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

Ovenstående kode bruger `sync.OnceFunc` til at udskrive `Hello, World!`. I denne kode oprettes funktionen `once` med `sync.OnceFunc`, og selvom `once` kaldes flere gange, udskrives `Hello, World!` kun én gang.

#### OnceValue

`OnceValue` sørger ikke blot for, at den pågældende funktion kun køres præcis én gang, men gemmer også funktionens returværdi, så den gemte værdi returneres ved de efterfølgende kald.

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

Ovenstående kode bruger `sync.OnceValue` til at øge variablen `c` med 1. I denne kode oprettes funktionen `once` med `sync.OnceValue`, og selvom `once` kaldes flere gange, returnerer den 1, fordi variablen `c` kun er blevet øget én gang.

#### OnceValues

`OnceValues` fungerer på samme måde som `OnceValue`, men kan returnere flere værdier.

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

Ovenstående kode bruger `sync.OnceValues` til at øge variablen `c` med 1. I denne kode oprettes funktionen `once` med `sync.OnceValues`, og selvom `once` kaldes flere gange, returnerer den 1, fordi variablen `c` kun er blevet øget én gang.

### atomic

Pakken `atomic` er en pakke, der tilbyder atomare operationer. Pakken `atomic` tilbyder metoder som `Add`, `CompareAndSwap`, `Load`, `Store` og `Swap`, men for nylig anbefales det at bruge 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 et eksempel, der blev brugt tidligere. Koden bruger typen `atomic.Int64` til at øge variablen `c` atomisk. Med metoderne `Add` og `Load` kan variablen øges atomisk og læses. Desuden kan man gemme en værdi med metoden `Store`, udskifte en værdi med metoden `Swap`, og med metoden `CompareAndSwap` sammenligne en værdi og udskifte den, hvis den passer.

### cond

#### sync.Cond

Pakken `cond` er en pakke, der tilbyder betingelsesvariabler. Pakken `cond` kan oprettes med `sync.Cond`, og tilbyder metoderne `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!")
}
```

Ovenstående kode bruger `sync.Cond` til at vente, indtil variablen `ready` bliver `true`. I denne kode venter `sync.Cond`, indtil variablen `ready` bliver `true`, og derefter udskrives `Ready!`. På denne måde kan man med `sync.Cond` få flere goroutines til at vente, indtil en bestemt betingelse er opfyldt.

Ved at udnytte dette kan man implementere en simpel `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 man bruger `sync.Cond` på denne måde, kan man vente effektivt i stedet for at bruge stort CPU-forbrug med en `spin-lock`, og fortsætte, når betingelsen er opfyldt.

### semaphore

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

Pakken `semaphore` er en pakke, der tilbyder semaforer. Pakken `semaphore` kan oprettes med `golang.org/x/sync/semaphore.Semaphore`, og tilbyder metoderne `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)
}
```

Ovenstående kode opretter en semafor med `semaphore`, henter semaforen med metoden `Acquire` og frigiver den med metoden `Release`. I denne kode er det vist, hvordan man henter og frigiver en semafor med `semaphore`.

## Afslutning

Det grundlæggende indhold burde være tilstrækkeligt her. Ud fra indholdet i denne artikel håber jeg, at du kan forstå, hvordan man håndterer samtidighed med goroutines, og at du kan bruge det i praksis. Jeg håber, at denne artikel har været nyttig for dig. Tak.
