Concurrence et multithreading en Go : niveau avancé
Go (Golang) a été conçu dès le départ comme un langage pour écrire des applications évolutives et performantes. Au cœur de cela se trouve un puissant modèle de concurrence basé sur les goroutines et les canaux. Si vous maîtrisez déjà les exemples de base avec go func() et les canaux simples, il est temps de passer au niveau supérieur. Dans cet article, nous allons explorer les motifs avancés de travail avec la concurrence en Go qui vous aideront à écrire un code fiable, efficace et sûr.
1. Gestion du cycle de vie des goroutines : Context et WaitGroup
L'un des problèmes les plus courants est la fuite de goroutines. Une goroutine qui attend indéfiniment des données d'un canal ne se terminera jamais si le canal est fermé incorrectement. Pour gérer le cycle de vie des goroutines en Go, il existe deux outils principaux : sync.WaitGroup et context.Context.
sync.WaitGroup pour attendre la fin
Utilisez sync.WaitGroup lorsque vous devez attendre la fin d'un ensemble fixe de goroutines. C'est un mécanisme simple et efficace.
package main
import (\t"fmt"\t"sync"\t"time")
func worker(id int, wg *sync.WaitGroup) {\tdefer wg.Done() // Décrémente le compteur à la sortie\tfmt.Printf("Worker %d a commencé le travail", id)\ttime.Sleep(time.Second)\tfmt.Printf("Worker %d a terminé le travail", id)}
func main() {\tvar wg sync.WaitGroup
\tfor i := 1; i <= 3; i++ {\t\twg.Add(1) // Incrémente le compteur avant le lancement\t\tgo worker(i, &wg)\t}
\twg.Wait() // Bloque jusqu'à ce que le compteur soit à 0\tfmt.Println("Tous les workers ont terminé")}context.Context pour l'annulation et les délais
Lorsqu'une goroutine travaille en boucle ou effectue une opération longue, elle doit pouvoir se terminer correctement sur un signal externe. context.Context est le moyen standard de transmettre un signal d'annulation, de délai ou de timeout.
package main
import (\t"context"\t"fmt"\t"time")
func longRunningTask(ctx context.Context) {\tfor {\t\tselect {\t\tcase <-ctx.Done():\t\t\tfmt.Println("La goroutine a reçu un signal d'arrêt")\t\t\treturn\t\tdefault:\t\t\ttime.Sleep(500 * time.Millisecond)\t\t\tfmt.Println("Je travaille...")\t\t}\t}}
func main() {\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\tdefer cancel() // Important d'appeler cancel pour libérer les ressources
\tgo longRunningTask(ctx)
\ttime.Sleep(3 * time.Second)\tfmt.Println("Main terminé")}2. Canaux : mise en mémoire tampon, fermeture et motif Fan-Out/Fan-In
Les canaux sont le principal moyen de synchronisation et de transfert de données entre les goroutines. Au niveau avancé, il est important de comprendre la différence entre les canaux tamponnés et non tamponnés, de les fermer correctement et d'utiliser les motifs d'agrégation.
Tamponnés vs Non tamponnés
Un canal non tamponné (sans tampon) bloque l'expéditeur jusqu'à ce que le destinataire lise les données. Un canal tamponné permet d'envoyer plusieurs messages sans attendre, jusqu'à ce que le tampon soit plein. Le choix dépend de la tâche : pour une synchronisation stricte, utilisez des canaux non tamponnés ; pour lisser la charge, utilisez des canaux tamponnés.
Motif Fan-Out/Fan-In
C'est l'un des motifs les plus puissants. Fan-Out consiste à distribuer des tâches à plusieurs goroutines (workers). Fan-In consiste à collecter les résultats de plusieurs goroutines dans un seul canal.
package main
import (\t"fmt"\t"sync")
func producer(nums ...int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor _, n := range nums {\t\t\tout <- n\t\t}\t\tclose(out)\t}()\treturn out}
func square(in <-chan int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor n := range in {\t\t\tout <- n * n\t\t}\t\tclose(out)\t}()\treturn out}
func merge(channels ...<-chan int) <-chan int {\t