Concurrence et multithreading en Go : niveau avancé
\n\nLa concurrence (concurrency) est l'une des caractéristiques clés du langage Go, intégrée dans son architecture au niveau du runtime et de la syntaxe. Contrairement à de nombreux autres langages où le multithreading est implémenté via des bibliothèques complexes et nécessite une gestion manuelle des threads, Go propose des goroutines légères et des canaux (channels). Au niveau avancé, il ne s'agit pas simplement de lancer plusieurs tâches, mais de les synchroniser correctement, d'éviter les conditions de course et d'utiliser efficacement les ressources système. Dans cet article, nous aborderons les techniques avancées de travail avec la concurrence en Go.
\n\n1. Goroutines : gestion du cycle de vie et priorités
\n\nLes goroutines sont des threads légers gérés par le runtime de Go. Elles sont lancées à l'aide du mot-clé go. Cependant, au niveau avancé, il est nécessaire de comprendre comment contrôler leur exécution et leur terminaison.
Lancement de base d'une goroutine :
\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc worker(id int) {\n fmt.Printf("Worker %d starting\\n", id)\n time.Sleep(time.Second)\n fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n for i := 1; i <= 5; i++ {\n go worker(i)\n }\n time.Sleep(2 * time.Second) // Solution de fortune : on attend la fin\n}\n\nProblème : dans cet exemple, nous utilisons time.Sleep pour attendre, ce qui n'est pas fiable. Au niveau avancé, on utilise les canaux et sync.WaitGroup.
2. Synchronisation avec sync.WaitGroup
\n\nsync.WaitGroup permet d'attendre la fin d'un nombre donné de goroutines. C'est un outil basique mais puissant pour la synchronisation.
package main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, wg *sync.WaitGroup) {\n defer wg.Done() // Décrémente le compteur à la fin\n fmt.Printf("Worker %d starting\\n", id)\n time.Sleep(time.Second)\n fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n var wg sync.WaitGroup\n for i := 1; i <= 5; i++ {\n wg.Add(1) // Incrémente le compteur avant le lancement\n go worker(i, &wg)\n }\n wg.Wait() // Bloque jusqu'à ce que le compteur soit à zéro\n fmt.Println("All workers completed")\n}\n\nImportant : wg.Add() doit être appelé avant le lancement de la goroutine, sinon une condition de course est possible. Utilisez defer wg.Done() à l'intérieur de la goroutine pour garantir la décrémentation du compteur même en cas de panique.
3. Canaux : bufférisation, direction et select
\n\nLes canaux sont le mécanisme principal d'échange de données entre les goroutines. Au niveau avancé, on utilise les canaux bufférisés, les canaux unidirectionnels et l'opérateur select.
3.1. Canaux bufférisés
\nLes canaux bufférisés permettent d'envoyer des données sans réception immédiate. Cela réduit les blocages et augmente le débit.
\npackage main\n\nimport "fmt"\n\nfunc main() {\n ch := make(chan int, 3) // canal avec un buffer de 3 éléments\n ch <- 1\n ch <- 2\n ch <- 3\n // ch <- 4 // blocage, car le buffer est plein\n fmt.Println(<-ch)\n fmt.Println(<-ch)\n fmt.Println(<-ch)\n}\n\n3.2. Canaux unidirectionnels
\nPour la sécurité des types et pour indiquer explicitement le rôle d'un canal, on utilise les canaux unidirectionnels : chan<- (envoi uniquement) et <-chan (réception uniquement).
package main\n\nimport "fmt"\n\nfunc sender(out chan<- int) {\n for i := 0; i < 5; i++ {\n out <- i\n }\n close(out)\n}\n\nfunc receiver(in <-chan int) {\n for val := range in {\n fmt.Println(val)\n }\n}\n\nfunc main() {\n ch := make(chan int)\n go sender(ch)\n receiver(ch)\n}\n\n3.3. Opérateur select
\nselect permet à une goroutine d'attendre plusieurs canaux simultanément. C'est