Concurrence en Go

La concurrence est la capacité d'un programme à exécuter plusieurs tâches simultanément.

Le langage Go prend en charge la concurrence, offrant un moyen concis et efficace de la mettre en œuvre via les goroutines et les canaux.

Goroutines:

  • L'unité d'exécution concurrente en Go, semblable à des threads légers.
  • L'ordonnancement des Goroutines est géré par le runtime Go ; les utilisateurs n'ont pas besoin d'allouer manuellement des threads.
  • UtilisationgoLe mot-clé démarre une Goroutine.
  • Les Goroutines sont non bloquantes et peuvent exécuter efficacement des dizaines de milliers de goroutines.

Canal:

  • Le mécanisme utilisé pour la communication entre Goroutines en Go.
  • Prend en charge la synchronisation et le partage de données, en évitant les mécanismes de verrouillage explicites.
  • UtilisationchanCréé par le mot-clé, via<-L'opérateur envoie et reçoit des données.

Ordonnanceur :

L'ordonnanceur de Go est basé sur le modèle GMP. L'ordonnanceur affecte les Goroutines aux threads système pour l'exécution, et gère efficacement la concurrence grâce à la coordination de M et P.

  • G:Goroutine.
  • M: Thread système (Machine).
  • P: Processeur logique.

Goroutine

Les goroutines sont des threads légers, et leur ordonnancement est géré par le runtime Go.

Format de syntaxe d'une goroutine :

go 函数名( 参数列表 )

Par exemple :

go f(x, y, z)

Démarrer une nouvelle goroutine :

f(x, y, z)

Go permet à l'instruction go de démarrer un nouveau thread d'exécution, à savoir une goroutine, exécutant une fonction dans une goroutine distincte nouvellement créée. Toutes les goroutines d'un même programme partagent le même espace d'adressage.

Exemple

package main

import (
        "fmt"
        "time"
)

func sayHello() {
        for i := 0; i < 5; i++ {
                fmt.Println("Hello")
                time.Sleep(100 * time.Millisecond)
        }
}

func main() {
        go sayHello() // Démarrer une goroutine
        for i := 0; i < 5; i++ {
                fmt.Println("Main")
                time.Sleep(100 * time.Millisecond)
        }
}

En exécutant le code ci-dessus, vous verrez Main et Hello dans la sortie. La sortie n'a pas d'ordre fixe car ils sont exécutés par deux goroutines :

Main
Hello
Main
Hello
...

Canal

Les canaux sont utilisés pour le transfert de données entre les Goroutines.

Les canaux peuvent être utilisés pour une exécution synchronisée et une communication entre deux goroutines en passant une valeur d'un type spécifié.

UtilisationmakeLa fonction crée un canal, en utilisant<-L'opérateur <- envoie et reçoit des données. Si aucune direction n'est spécifiée, il s'agit d'un canal bidirectionnel.

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

Déclarer un canal est très simple ; nous utilisons simplement le mot-clé chan. Un canal doit être créé avant utilisation :

ch := make(chan int)

RemarquePar défaut, les canaux ne sont pas tamponnés. Lorsque l'expéditeur envoie des données, il doit y avoir un récepteur correspondant qui reçoit les données en même temps.

L'exemple suivant utilise deux goroutines pour calculer la somme de nombres. Après que les goroutines ont terminé leurs calculs, il calcule la somme des deux résultats :

Exemple

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // Envoyer la somme au canal c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // Recevoir depuis le canal c

    fmt.Println(x, y, x+y)
}

Le résultat de sortie est:

-5 17 12

Tampon de canal

Les canaux peuvent avoir un tampon, dont la taille est spécifiée par le deuxième argument de make:

ch := make(chan int, 100)

Les canaux tamponnés permettent à l'envoi de données par l'émetteur et à la réception de données par le récepteur d'être dans un état asynchrone. Autrement dit, les données envoyées par l'émetteur peuvent être placées dans le tampon et attendre que le récepteur les récupère, plutôt que d'exiger que le récepteur les récupère immédiatement.

Cependant, comme la taille du tampon est limitée, il doit toujours y avoir un récepteur pour recevoir des données. Sinon, une fois le tampon plein, l'émetteur ne peut plus envoyer de données.

RemarqueSi un canal n'est pas tamponné, l'émetteur se bloque jusqu'à ce que le récepteur ait reçu une valeur du canal. Si un canal est tamponné, l'émetteur se bloque jusqu'à ce que la valeur envoyée soit copiée dans le tampon ; si le tampon est plein, cela signifie attendre qu'un récepteur récupère une valeur. Le récepteur se bloque jusqu'à ce qu'il y ait une valeur à recevoir.

Exemple

package main

import "fmt"

func main() {
    // Ici, nous définissons un canal tamponné qui peut stocker des entiers
    // La taille du tampon est 2
    ch := make(chan int, 2)

    // Comme ch est un canal tamponné, nous pouvons envoyer deux valeurs simultanément
    // sans avoir besoin de lire les données de manière synchrone immédiatement
    ch <- 1
    ch <- 2

    // Recevoir ces deux valeurs
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

La sortie d'exécution est :

1
2

Itération sur les canaux avec range et fermeture des canaux en Go

Go utilise le mot-clé range pour parcourir les données reçues, de façon similaire aux tableaux ou aux slices. Le format est le suivant :

v, ok := <-ch

Si le canal ne reçoit aucune donnée, ok devient false, puis le canal peut être fermé à l'aide de close().close()fonction pour fermer.

Exemple

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    // La fonction range itère sur chaque valeur reçue du canal, car après que c a envoyé 10
    // valeurs, il ferme le canal, donc ici la fonction range, après avoir reçu 10 valeurs
    // puis se termine. Si le canal c ci-dessus n'est pas fermé, alors la fonction range ne
    // se terminera pas, et donc elle se bloque lors de la réception de la 11e valeur.
    for i := range c {
        fmt.Println(i)
    }
}

La sortie d'exécution est :

0
1
1
2
3
5
8
13
21
34

Instruction Select

selectL'instruction select permet à une goroutine d'attendre plusieurs opérations de communication.selectIl bloquera jusqu'à ce que l'un de ses cas puisse s'exécuter :

Exemple

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

Dans le code ci-dessus,fibonaccigoroutine sur le canalcEnvoyer la suite de Fibonacci sur le canal, et à la réceptionquitQuitter lorsqu'un signal est reçu depuis le canal.

La sortie d'exécution est :

0
1
1
2
3
5
8
13
21
34
quit

Utilisation de WaitGroup

sync.WaitGroup est utilisé pour attendre que plusieurs Goroutines se terminent.

Synchronisation de plusieurs goroutines :

Exemple

package main

import (
        "fmt"
        "sync"
)

func worker(id int, wg *sync.WaitGroup) {
        defer wg.Done() // Appeler Done() lorsque la goroutine se termine
        fmt.Printf("Worker %d a démarré\n", id)
        fmt.Printf("Worker %d a terminé\n", id)
}

func main() {
        var wg sync.WaitGroup

        for i := 1; i <= 3; i++ {
                wg.Add(1) // Incrémenter le compteur
                go worker(i, &wg)
        }

        wg.Wait() // Attendre que toutes les goroutines soient terminées
        fmt.Println("Tous les workers ont terminé")
}

Pour le code ci-dessus, la sortie d'exécution est la suivante :

Worker 1 started
Worker 1 finished
Worker 2 started
Worker 2 finished
Worker 3 started
Worker 3 finished
All workers done

Fonctionnalités avancées

Canal tamponné:

Créer un canal tamponné.

ch := make(chan int, 2)

Contexte:

Utilisé pour contrôler le cycle de vie des Goroutines.

context.WithCancel、context.WithTimeout.

Mutex et RWMutex :

sync.Mutex fournit un verrou d'exclusion mutuelle pour protéger les ressources partagées.

var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()

Résumé de la programmation concurrente

Le langage Go offre un support puissant de la concurrence via les Goroutines et les canaux, simplifiant la complexité du modèle de threads traditionnel. Avec l'ordonnanceur et les outils de synchronisation, des programmes concurrents hautes performances peuvent être facilement implémentés.

  • Goroutinesest un thread léger, utilisantgoDémarré par le mot-clé.
  • CanauxUtilisés pour la communication entre goroutines.
  • Instruction SelectUtilisée pour attendre plusieurs opérations de canal.

FAQ

Interblocage :

  • Exemple : Toutes les goroutines sont en attente, mais aucune donnée n'est disponible.
  • Solution : Évitez l'attente infinie et fermez correctement les canaux.

Course de données :

  • Exemple : Plusieurs goroutines accèdent simultanément à la même variable.
  • Solution : utilisez Mutex ou Channel pour synchroniser l'accès.
autres extensions