ProgrammationDéveloppeur Backend

Comment fonctionnent les goroutines et le planificateur Go, et pourquoi est-il important de bien gérer l'exécution concurrente des tâches ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

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 :

  • Création instantanée et peu coûteuse de goroutines (dizaines de milliers de fois moins coûteuses qu'un thread système).
  • Interaction directe via des canaux, assurant la synchronisation et l'échange de données.
  • Nécessité de contrôler manuellement la terminaison des goroutines (quelles goroutines attendent, qui les interrompt, comment signaler l'arrêt).

Questions pièges.

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

Erreurs courantes et anti-patterns

  • Lancement de goroutines sans contrôle de leur terminaison.
  • Capture de variables de boucle sans les passer dans des fonctions anonymes.
  • Surcharge du système à cause des "goroutines qui fuient" (fuites de goroutines non terminées).
  • Ignorer les erreurs de synchronisation lors des échanges via des canaux.

Exemple de la vie réelle

Cas négatif

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 :

  • Grande rapidité de démarrage.
  • Simplicité de mise à l'échelle.

Inconvénients :

  • Fuite de mémoire.
  • Augmentation du temps de réponse.
  • Terminaison imprévisible.

Cas positif

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 :

  • Cycle de vie prévisible.
  • Contrôle de la terminaison.
  • Facile à mettre à l'échelle.

Inconvénients :

  • Nécessité d'écrire explicitement la logique d'annulation et de synchronisation.
  • Architecture du programme légèrement plus complexe.