GoSuda

Principes fondamentaux des Goroutines

By hamori
views ...

Goroutine

Si l'on demande aux Gophers de citer les avantages de Golang, la concurrence est un sujet fréquemment abordé. Le fondement de ce concept réside dans les goroutines, qui sont légères et faciles à gérer. J'ai rédigé un bref aperçu à ce sujet.

Concurrence vs Parallélisme

Avant de comprendre les goroutines, je souhaite clarifier deux concepts souvent confondus.

  • Concurrence : La concurrence consiste à gérer de nombreuses tâches à la fois. Cela ne signifie pas nécessairement qu'elles s'exécutent simultanément. C'est un concept structurel et logique où plusieurs tâches sont divisées en petites unités et exécutées en alternance, donnant à l'utilisateur l'impression que plusieurs tâches sont traitées simultanément. La concurrence est possible même avec un seul cœur.
  • Parallélisme : Le parallélisme consiste à "traiter plusieurs tâches simultanément sur plusieurs cœurs". Il s'agit littéralement d'exécuter des tâches en parallèle, en effectuant différentes opérations en même temps.

Les goroutines permettent d'implémenter facilement la concurrence via le planificateur d'exécution de Go, et elles exploitent naturellement le parallélisme grâce au paramètre GOMAXPROCS.

Les multithreads Java, qui sont souvent très utilisés, sont un exemple typique de parallélisme.

Pourquoi les goroutines sont-elles avantageuses ?

Légèreté (lightweight)

Le coût de création est très faible par rapport à d'autres langages. La question se pose alors de savoir pourquoi Golang en utilise si peu. La raison en est que leur création est gérée en interne par le runtime Go. C'est parce qu'il s'agit de threads logiques légers, plus petits que les unités de thread OS, nécessitant une pile initiale d'environ 2 Ko et pouvant varier dynamiquement en ajoutant de la pile selon l'implémentation de l'utilisateur.

Gérées par unités de pile, leur création et suppression sont très rapides et peu coûteuses, ce qui permet de gérer des millions de goroutines sans surcharge. Grâce à cela, les Goroutines peuvent minimiser l'intervention du noyau OS grâce au planificateur d'exécution.

Performance (performance)

Premièrement, comme expliqué précédemment, les Goroutines nécessitent moins d'intervention du noyau OS, ce qui réduit le coût des commutations de contexte au niveau utilisateur par rapport aux threads OS, permettant ainsi des changements de tâche rapides.

En outre, elles sont gérées en étant allouées aux threads OS en utilisant le modèle M:N. Cela permet de traiter les tâches avec un nombre réduit de threads, sans avoir besoin d'un grand pool de threads OS. Par exemple, si une Goroutine entre en état de blocage, comme lors d'un appel système, le runtime Go exécute une autre Goroutine sur le thread OS, permettant ainsi au thread OS de ne pas rester inactif et d'utiliser efficacement le CPU pour un traitement rapide.

En conséquence, Golang peut offrir des performances supérieures à d'autres langages, en particulier pour les opérations d'I/O.

Concis (concise)

Un avantage majeur est la facilité avec laquelle une fonction peut être traitée en utilisant simplement le mot-clé go lorsqu'une concurrence est requise.

L'utilisation de verrous complexes tels que Mutex et Semaphore est nécessaire, et l'utilisation de verrous implique inévitablement la considération d'états de DeadLock, ce qui rend nécessaire une phase de conception complexe dès le début du développement.

Les Goroutines, suivant la philosophie "ne communiquez pas en partageant la mémoire, mais partagez la mémoire en communiquant", encouragent le transfert de données via des Channel et SELECT prend en charge la fonctionnalité de traitement des Channel dès que les données sont prêtes. De plus, sync.WaitGroup permet d'attendre simplement la fin de toutes les goroutines, facilitant ainsi la gestion du flux de travail. Grâce à ces outils, il est possible de prévenir les problèmes de compétition de données entre threads et d'assurer un traitement concurrent plus sûr.

De plus, l'utilisation du context permet de contrôler le cycle de vie, l'annulation, les timeouts, les deadlines et la portée des requêtes au niveau utilisateur, garantissant ainsi un certain degré de stabilité.

Traitement parallèle des Goroutines (GOMAXPROCS)

Bien que nous ayons discuté des avantages de la concurrence des goroutines, vous pourriez vous demander si le parallélisme n'est pas pris en charge. Le nombre de cœurs de CPU a récemment dépassé la dizaine, contrairement au passé, et même les PC domestiques disposent d'un nombre non négligeable de cœurs.

Cependant, les Goroutines effectuent également des opérations parallèles, et c'est ce que fait GOMAXPROCS.

Si GOMAXPROCS n'est pas configuré, il est défini différemment selon les versions.

  1. Avant la version 1.5 : La valeur par défaut est 1. Si plus de 1 est nécessaire, la configuration est obligatoire via une méthode telle que runtime.GOMAXPOCS(runtime.NumCPU()).

  2. De 1.5 à 1.24 : A été modifié pour utiliser tous les cœurs logiques disponibles. À partir de ce moment, les développeurs n'ont plus besoin de le configurer, sauf en cas de contraintes spécifiques.

  3. 1.25 : Comme il s'agit d'un langage populaire dans les environnements de conteneurs, il vérifie le cGroup sur Linux pour déterminer la limitation du CPU configurée pour le conteneur.

    Ainsi, si le nombre de cœurs logiques est de 10 et que la limite de CPU est de 5, GOMAXPROCS sera défini sur la valeur inférieure de 5.

La modification de la version 1.25 représente un changement très significatif. C'est parce que l'utilité du langage dans les environnements de conteneurs a augmenté. Cela a permis d'éviter la création inutile de threads et les commutations de contexte, prévenant ainsi le throttling du CPU.

 1package main
 2
 3import (
 4	"fmt"
 5	"math/rand"
 6	"runtime"
 7	"time"
 8)
 9
10func exe(name int, wg *sync.WaitGroup) {
11	defer wg.Done()
12
13	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Démarrage
14	time.Sleep(10 * time.Millisecond) // Délai pour simuler le travail
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Démarrage
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // Utiliser seulement 2 cœurs CPU
20	wg := sync.WaitGroup();
21  goroutineCount := 10
22	wg.Add(goroutineCount)
23
24	for i := 0; i < goroutineCount; i++ {
25		go exe(i, &wg)
26	}
27
28	fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Attente de la fin de toutes les goroutines...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // Toutes les tâches sont terminées.
31
32}
33

Planificateur de Goroutines (Modèle M:N)

En allant un peu plus loin dans le concept du modèle M:N utilisé pour allouer et gérer les Goroutines aux threads OS, on trouve le modèle GMP des Goroutines.

  • G (Goroutine) : La plus petite unité de travail exécutée dans Go.
  • M (Machine) : Le thread OS (emplacement réel du travail).
  • P (Processor) : Le processus logique géré par le runtime Go.

P possède également une file d'exécution locale (Local Run Queue) et agit comme un planificateur pour attribuer les G allouées aux M. En bref, une goroutine...

Le processus de fonctionnement de GMP est le suivant :

  1. Lorsqu'une G (Goroutine) est créée, elle est allouée à la file d'exécution locale d'un P (Processor).
  2. Le P (Processor) alloue la G (Goroutine) de sa file d'exécution locale à un M (Machine).
  3. Le M (Machine) renvoie l'état de la G (Goroutine) : bloqué, terminé ou préempté.
  4. Work-Stealing (vol de travail) : Si la file d'exécution locale d'un P devient vide, un autre P vérifie la file globale. S'il n'y a pas non plus de G (Goroutine) là-bas, il "vole" le travail d'un autre P (Processor) local pour s'assurer que tous les M fonctionnent sans interruption.
  5. Gestion des appels système (Blocking) : Si une G (Goroutine) est bloquée pendant son exécution, le M (Machine) entre en état d'attente. À ce moment, le P (Processor) se sépare du M (Machine) bloqué et se combine avec un autre M (Machine) pour exécuter la G (Goroutine) suivante. Pendant ce temps, il n'y a pas de gaspillage de CPU pendant le temps d'attente des opérations I/O.
  6. Si une G (Goroutine) préempte (preempted) pendant une longue période, elle cède le temps d'exécution à d'autres G (Goroutine).

Le GC (Garbage Collector) de Golang s'exécute également sur les Goroutines, permettant de nettoyer la mémoire en parallèle avec une interruption minimale de l'exécution de l'application (STW), utilisant ainsi efficacement les ressources système.

Enfin, Golang est l'un des points forts du langage, et il en existe bien d'autres, alors j'espère que de nombreux développeurs apprécieront Go.

Merci.