---
id: 01933e13f963942e4c9bc8c3c89fbc2b
author: snowmerak
title: Kit de démarrage sur la concurrence en Go
description: Présentation de divers outils et techniques pour la gestion de la concurrence en Go, notamment les goroutines, les canaux et les mutex.
language: fr
date: 2024-10-18T05:51:30.010680581Z
path: /blog/posts/go-concurrency-starter-pack-z221a3399
category: concurrency
---

## Aperçu

### Brève introduction

Le langage Go dispose de nombreux outils pour la gestion de la concurrence. Dans cet article, nous allons présenter certains d'entre eux ainsi que quelques astuces.

### Goroutines ?

Une goroutine est un nouveau modèle de concurrence pris en charge par le langage Go. En général, pour effectuer plusieurs tâches simultanément, un programme reçoit des threads OS du système d'exploitation et exécute des tâches en parallèle à raison du nombre de cœurs. Et pour réaliser une concurrence à plus petite échelle, on crée des threads verts au niveau utilisateur (userland), de sorte que plusieurs threads verts s'exécutent à tour de rôle au sein d'un même thread OS. Cependant, les goroutines rendent cette forme de threads verts plus légère et plus efficace. Ces goroutines utilisent moins de mémoire que les threads et peuvent être créées et commutées plus rapidement que ces derniers.

Pour utiliser une goroutine, il suffit d'employer le mot-clé `go`. Cela permet, lors de la rédaction d'un programme, d'exécuter intuitivement du code synchrone sous forme de code asynchrone.

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

Ce code transforme simplement un code synchrone, qui attend une seconde puis affiche `Hello, World!`, en un flux asynchrone. Si l'exemple actuel est simple, la transformation d'un code synchrone un peu plus complexe en code asynchrone améliore la lisibilité, la visibilité et la compréhension du code, par rapport aux approches classiques telles que async/await ou promise.

Toutefois, dans de nombreux cas, des goroutines mal conçues sont produites lorsqu'on ne comprend pas le simple appel asynchrone d'un code synchrone, ni les flux de type `fork & join` (semblables à une logique de diviser pour régner). Pour se prémunir contre ces situations, cet article présentera quelques méthodes et techniques.

## Gestion de la concurrence

### context

Il peut sembler surprenant que `context` soit la première technique présentée. Pourtant, en Go, `context` joue un rôle remarquable dans la gestion de l'arbre complet des tâches, bien au-delà de la simple annulation. Pour ceux qui ne le connaissent pas, je vais brièvement présenter ce package.

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

Le code ci-dessus utilise `context` pour afficher `Context is done!` après une seconde. `context` permet de vérifier si une annulation a eu lieu via la méthode `Done()`, et fournit diverses méthodes d'annulation telles que `WithCancel`, `WithTimeout`, `WithDeadline` et `WithValue`.

Créons un exemple simple. Supposons que vous écriviez un code qui utilise le motif `aggregator` pour récupérer `user`, `post` et `comment`, et que toutes les requêtes doivent être effectuées dans un délai de 2 secondes. On peut alors écrire le code suivant.

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

Si toutes les données ne sont pas récupérées en 2 secondes, le code ci-dessus affiche `Timeout!`, et s'il les récupère toutes, il affiche `All data is fetched!`. De cette manière, en utilisant `context`, on peut gérer facilement l'annulation et les délais d'expiration, même dans un code où plusieurs goroutines fonctionnent.

Les différentes fonctions et méthodes liées à context sont consultables sur [godoc context](https://pkg.go.dev/context). J'espère que vous pourrez apprendre les éléments simples et les utiliser avec aisance.

### channel

#### unbuffered channel

`channel` est un outil de communication entre goroutines. Un `channel` se crée avec `make(chan T)`. Ici, `T` désigne le type des données que ce `channel` transmettra. On envoie et reçoit des données avec `<-`, et on peut fermer un `channel` avec `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)
    }
}
```

Le code ci-dessus utilise `channel` pour afficher 1 et 2. Cet exemple se contente de montrer l'envoi et la réception de valeurs dans un channel, mais `channel` offre davantage de fonctionnalités. Commençons par examiner `buffered channel` et `unbuffered channel`. Avant de commencer, l'exemple présenté plus haut est un `unbuffered channel` : l'action d'envoyer des données dans le canal et celle de les recevoir doivent avoir lieu simultanément. Si ces actions ne se produisent pas en même temps, un deadlock peut survenir.

#### buffered channel

Et si, au lieu d'un simple affichage, le code précédent effectuait deux processus lourds ? Si le second processus, qui lit et traite les données, est retardé pendant longtemps, le premier processus sera également bloqué pendant ce temps. Pour éviter cette situation, on peut utiliser `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)
    }
}
```

Le code ci-dessus utilise `buffered channel` pour afficher 1 et 2. Dans ce code, grâce à `buffered channel`, l'action d'envoyer des données dans le canal et celle de les recevoir n'ont pas besoin d'avoir lieu simultanément. Ainsi, en ajoutant un tampon au canal, on dispose d'une marge correspondant à sa longueur, ce qui permet d'éviter les retards de travail causés par l'influence des tâches en aval.

#### select

Lorsqu'on manipule plusieurs canaux, la syntaxe `select` permet de mettre en œuvre facilement une structure `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)
        }
    }
}
```

Le code ci-dessus crée trois canaux qui transmettent périodiquement 1, 2 et 3, puis utilise `select` pour recevoir les valeurs de ces canaux et les afficher. De cette manière, `select` permet de recevoir simultanément des données de plusieurs canaux et de traiter chaque valeur dès qu'elle est reçue.

#### for range

On peut facilement recevoir des données d'un `channel` à l'aide de `for range`. Lorsque `for range` est appliqué à un canal, la boucle s'exécute à chaque ajout de données dans ce canal, et se termine lorsque le canal est fermé.

```go
package main

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

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

Le code ci-dessus utilise `channel` pour afficher 1 et 2. Dans cet exemple, `for range` reçoit et affiche les données à chaque ajout dans le canal, puis termine la boucle lorsque le canal est fermé.

Comme on l'a fait à plusieurs reprises plus haut, cette syntaxe peut aussi servir de simple mécanisme de synchronisation.

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

Le code ci-dessus attend une seconde puis affiche `Hello, World!`. Dans ce code, un `channel` est utilisé pour transformer un code synchrone en code asynchrone. De cette manière, en employant `channel`, on peut facilement transformer un code synchrone en code asynchrone et définir un point de `join`.

#### etc

1. Envoyer ou recevoir des données sur un nil channel peut provoquer une boucle infinie et un deadlock.
2. Envoyer des données après la fermeture d'un channel provoque une panic.
3. Il n'est pas nécessaire de fermer explicitement un channel : le GC le collecte et le ferme alors.

### mutex

#### spinlock

`spinlock` est une méthode de synchronisation qui tente en boucle d'acquérir le verrou en continu. En Go, il est facile d'implémenter un spinlock à l'aide de pointeurs.

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

Le code ci-dessus est l'implémentation du package `spinlock`. Ici, `SpinLock` est implémenté à l'aide du package `sync/atomic`. La méthode `Lock` tente d'acquérir le verrou via `atomic.CompareAndSwapUintptr`, et la méthode `Unlock` le libère via `atomic.StoreUintptr`. Comme cette approche tente d'acquérir le verrou sans relâche, elle consomme continuellement du CPU jusqu'à l'obtention du verrou, et peut donc entrer dans une boucle infinie. Par conséquent, il est préférable d'utiliser `spinlock` pour une synchronisation simple ou seulement pendant de courtes durées.

#### sync.Mutex

`mutex` est un outil de synchronisation entre goroutines. Le `mutex` fourni par le package `sync` propose les méthodes `Lock`, `Unlock`, `RLock`, `RUnlock`, etc. Un `mutex` se crée avec `sync.Mutex`, et il est également possible d'utiliser un verrou de lecture/écriture avec `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)
}
```

Dans le code ci-dessus, deux goroutines accèdent presque simultanément à la même variable `count`. En utilisant `mutex` pour transformer le code qui accède à la variable `count` en section critique, on peut empêcher l'accès concurrent à `count`. Ainsi, ce code affichera toujours `2`, quel que soit le nombre d'exécutions.

#### sync.RWMutex

`sync.RWMutex` est un `mutex` qui permet de distinguer et d'utiliser séparément le verrou de lecture et le verrou d'écriture. On peut poser et libérer un verrou de lecture à l'aide des méthodes `RLock` et `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
}
```

Le code ci-dessus implémente `ConcurrentMap` à l'aide de `sync.RWMutex`. Dans ce code, la méthode `Get` pose un verrou de lecture et la méthode `Set` pose un verrou d'écriture, ce qui permet d'accéder et de modifier la map `data` en toute sécurité. Le verrou de lecture est utile lorsque les opérations de simple lecture sont nombreuses : en ne posant qu'un verrou de lecture, sans verrou d'écriture, plusieurs goroutines peuvent lire simultanément. Ainsi, lorsqu'aucune modification d'état n'est nécessaire et qu'un verrou d'écriture n'est pas requis, n'utiliser qu'un verrou de lecture permet d'améliorer les performances.

#### fakelock

`fakelock` est une astuce simple consistant à implémenter `sync.Locker`. Cette structure fournit les mêmes méthodes que `sync.Mutex`, mais n'effectue aucune opération réelle.

```go
package fakelock

type FakeLock struct{}

func (f *FakeLock) Lock() {}

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

Le code ci-dessus est l'implémentation du package `fakelock`. Ce package implémente `sync.Locker` et fournit les méthodes `Lock` et `Unlock`, mais en réalité il ne fait absolument rien. Je reviendrai sur la raison de l'existence de ce code lorsque l'occasion se présentera.

### waitgroup

#### sync.WaitGroup

`sync.WaitGroup` est un outil qui attend que toutes les tâches des goroutines soient terminées. Il fournit les méthodes `Add`, `Done` et `Wait` : `Add` ajoute le nombre de goroutines, `Done` signale qu'une goroutine a terminé sa tâche, et `Wait` attend que toutes les goroutines aient terminé leur travail.

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

Le code ci-dessus utilise `sync.WaitGroup` pour que 100 goroutines ajoutent simultanément une valeur à la variable `c`. Ici, `sync.WaitGroup` attend que toutes les goroutines soient terminées, puis affiche la valeur accumulée dans `c`. Pour un `fork & join` de quelques tâches seulement, l'utilisation des canaux suffit, mais pour un `fork & join` d'un grand nombre de tâches, `sync.WaitGroup` est également un très bon choix.

#### with slice

Combiné à un slice, `waitgroup` peut devenir un excellent outil pour gérer des tâches exécutées simultanément sans verrou.

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

Le code ci-dessus utilise uniquement `waitgroup` pour que chaque goroutine génère simultanément un entier aléatoire parmi 10 et le stocke à l'indice qui lui a été attribué. Ici, `waitgroup` attend que toutes les goroutines soient terminées, puis affiche `Done`. De cette manière, avec `waitgroup`, plusieurs goroutines exécutent leurs tâches simultanément, stockent les données sans verrou jusqu'à la fin de toutes les goroutines, puis effectuent un post-traitement global une fois le travail terminé.

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

`errgroup` est un package qui étend `sync.WaitGroup`. Contrairement à `sync.WaitGroup`, `errgroup` annule toutes les goroutines et renvoie l'erreur dès qu'une des tâches des goroutines produit une erreur.

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

Le code ci-dessus utilise `errgroup` pour créer 10 goroutines, et déclenche volontairement une erreur dans la cinquième goroutine afin de montrer le cas où une erreur survient. En pratique, il suffit d'utiliser `errgroup` pour créer des goroutines et de mettre en place divers post-traitements lorsqu'une erreur survient dans chacune d'elles.

### once

Il s'agit d'un outil permettant d'exécuter du code qui ne doit être exécuté qu'une seule fois. Le code associé peut être exécuté à l'aide des constructeurs suivants.

```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` garantit simplement que la fonction concernée ne sera exécutée qu'une seule fois sur l'ensemble du programme.

```go
package main

import "sync"

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

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

Le code ci-dessus utilise `sync.OnceFunc` pour afficher `Hello, World!`. Dans ce code, la fonction `once` est créée avec `sync.OnceFunc`, et même si elle est appelée plusieurs fois, `Hello, World!` n'est affiché qu'une seule fois.

#### OnceValue

`OnceValue` ne se contente pas d'exécuter la fonction une seule fois sur l'ensemble du programme : il mémorise la valeur de retour de la fonction et renvoie cette valeur mémorisée lors des appels suivants.

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

Le code ci-dessus utilise `sync.OnceValue` pour incrémenter la variable `c` de 1. Dans ce code, la fonction `once` est créée avec `sync.OnceValue` et, même si elle est appelée plusieurs fois, la variable `c` n'est incrémentée qu'une seule fois, et la valeur 1 est renvoyée.

#### OnceValues

`OnceValues` fonctionne de manière identique à `OnceValue`, mais peut renvoyer plusieurs valeurs.

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

Le code ci-dessus utilise `sync.OnceValues` pour incrémenter la variable `c` de 1. Dans ce code, la fonction `once` est créée avec `sync.OnceValues` et, même si elle est appelée plusieurs fois, la variable `c` n'est incrémentée qu'une seule fois, et la valeur 1 est renvoyée.

### atomic

Le package `atomic` fournit des opérations atomiques. Il propose les méthodes `Add`, `CompareAndSwap`, `Load`, `Store`, `Swap`, etc., mais ces dernières années, l'utilisation des types `Int64`, `Uint64`, `Pointer`, etc. est recommandée.

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

C'est l'exemple utilisé précédemment. Il utilise le type `atomic.Int64` pour incrémenter la variable `c` de manière atomique. Les méthodes `Add` et `Load` permettent d'incrémenter la variable de manière atomique et de lire sa valeur. De plus, la méthode `Store` enregistre une valeur, `Swap` remplace une valeur, et `CompareAndSwap` compare une valeur et la remplace si elle correspond.

### cond

#### sync.Cond

Le package `cond` fournit des variables de condition. Il se crée avec `sync.Cond` et propose les méthodes `Wait`, `Signal` et `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!")
}
```

Le code ci-dessus utilise `sync.Cond` pour attendre que la variable `ready` devienne `true`. Dans ce code, `sync.Cond` permet d'attendre que `ready` devienne `true`, puis affiche `Ready!`. En utilisant `sync.Cond` de cette manière, plusieurs goroutines peuvent attendre simultanément qu'une condition particulière soit satisfaite.

En s'appuyant sur ce principe, on peut implémenter une simple `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
}
```

Ainsi, en utilisant `sync.Cond`, on peut attendre efficacement, au lieu de consommer beaucoup de CPU avec un `spin-lock`, et reprendre l'exécution dès que la condition est satisfaite.

### semaphore

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

Le package `semaphore` fournit des sémaphores. Il se crée avec `golang.org/x/sync/semaphore.Semaphore` et propose les méthodes `Acquire`, `Release` et `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)
}
```

Le code ci-dessus crée un sémaphore avec `semaphore`, acquiert le sémaphore avec la méthode `Acquire` puis le libère avec la méthode `Release`. Ce code montre comment acquérir et libérer un sémaphore à l'aide de `semaphore`.

## Conclusion

Ces éléments de base devraient suffire pour l'instant. J'espère qu'à partir du contenu de cet article, vous pourrez comprendre comment gérer la concurrence avec les goroutines et l'utiliser concrètement. J'espère que cet article vous aura été utile. Merci.
