Les goroutines sont des threads légers intégrés à l'architecture de Go depuis ses premières versions pour atteindre une concurrence efficace. Historiquement, l'idée de thread léger est née comme une tentative de contourner le coût élevé des threads système, ainsi qu'en raison de la forte demande pour des applications serveur scalables. Go a été conçu dès le départ comme un langage pour des systèmes serveur et réseau, où des millions de tâches doivent être traitées en parallèle.
Problème : La concurrence peut rapidement conduire à des conditions de course, des blocages et une augmentation de la consommation de mémoire si le cycle de vie des goroutines n'est pas contrôlé, si leur planification n'est pas prise en compte et si leur terminaison n'est pas gérée.
Solution : Les goroutines sont lancées via le mot-clé go. Le travail des goroutines est planifié par le planificateur Go, qui utilise le modèle M:N (M threads système servant N goroutines du langage Go). Pour gérer le cycle de vie, des canaux, WaitGroup, le contexte et le contrôle de fermeture des canaux sont utilisés.
Exemple de code :
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Worker %d started\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Caractéristiques clés :
Si dans main une goroutine n'est pas attendue explicitement, s'exécute-t-elle toujours ?
Non, l'exécution de main se termine — le processus se terminera indépendamment de l'état des goroutines enfants, et toutes les tâches ne seront pas exécutées.
Le lancement de go func(...) dans une boucle garantit-il que chaque goroutine obtiendra sa propre valeur des variables de boucle ?
Non, cela pose le problème de la capture de la variable de boucle, les goroutines peuvent travailler avec la même valeur de tranche/variable. Il faut utiliser la copie de la variable, par exemple, en la passant comme argument:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
Une goroutine peut-elle bloquer le planificateur Go et empêcher les autres de s'exécuter ?
Oui, si elle crée une boucle infinie ou très lourde sans points de commutation (par exemple, sans appels de fonction de temps ou yield), elle peut retenir le thread système — bien que cela contredise la philosophie de Go sur "la coopération multitâche". Par exemple, une fonction lourde sans blocages:
func busy() { for { // Pas d'attentes ou d'appels bloquants } }
Dans un microservice, une goroutine de lecture à partir d'une base de données est lancée périodiquement, mais on oublie de la terminer lors de l'annulation de la demande. En conséquence, des goroutines "pendues" restent, ce qui conduit au fil du temps à une consommation excessive de mémoire vive.
Avantages :
Inconvénients :
Le contexte est utilisé pour contrôler l'annulation des tâches, WaitGroup — pour gérer la terminaison de toutes les goroutines avant l'arrêt de l'application, et les canaux — pour transmettre correctement les données entre les exécuteurs.
Avantages :
Inconvénients :