La planification des goroutines dans le runtime Go (GMP)

Le runtime Go

Le runtime de Go peut être considéré comme l'environnement d'exécution pour les programmes Go, similaire à la JVM. Cependant, contrairement à la JVM, le runtime de Go est directement intégré au programme et exécuté sur le système d'exploitation, ce qui en fait une solution très efficace.

Le runtime inclut des fonctionnalités essentielles telles que la planification des goroutines, la collecte des déchets, et l'allocation de mémoire. Cet article se concentrera sur la planification des goroutines (modèle GMP).

Planification des goroutines

La planification des goroutines désigne la façon dont Go gère et exécute les goroutines. Cette planification repose sur le modèle GMP, où :

  • G (Goroutine) : représente une goroutine, comprenant des informations sur la pile, des pointeurs de code, et des états ;
  • M (Machine) : représente un thread de travail, alloué directement par le système d'exploitation ;
  • P (Processor) : un concept défini par Go, pas un CPU, qui contient les ressources nécessaires pour l'exécution des goroutines, comme des files d'attente locales, globales, et des compteurs.

Les relations entre G, M et P sont les suivantes :

  • Le nombre de P est déterminé par runtime.GOMAXPROCS, généralement égal au nombre de cœurs logiques du CPU. Par exemple, sur un CPU quadricœur avec huit threads, il y aura 8 P.
  • Le nombre de M est généralement supérieur au nombre de P. Pour qu'un M soit exécuté par le CPU, il doit d'abord obtenir un P. Les M sans P sont en sommeil.
  • Les G doivent être planifiés par un P pour être exécutés par un M. Sans P, les G sont en attente.
  • Chaque P possède une file d'attente locale (LRQ) pour les goroutines en attente. Les goroutines non assignées à un P sont placées dans une file d'attente globale (GRQ) et sont également en sommeil.

Pour un système à un seul cœur logique, le modèle GMP serait ainsi :

  • Les parties rouges représentent les éléments en sommeil ou suspendus, les jaunes en attente, et les vertes en cours d'exécution.
  • Deux threads (M0 et M1) sont initialisés, mais nous n'avons qu'un seul processeur (P). M1 n'ayant pas obtenu de P, il est en sommeil. M0, ayant obtenu P, exécute G0, tandis que LRQ contient trois autres G en attente.
  • GRQ contient G4, G5, G6, qui n'ont pas encore été assignés.

Le P effectue alors deux tâches principales :

  • Rotation de la file d'attente : les G de LRQ sont exécutés par M, chaque G étant mis en pause après un certain temps, puis remis en fin de file d'attente.
  • Vérification périodique : P vérifie si des G dans GRQ attendent d'être exécutés, pour éviter qu'ils ne soient bloqués indéfiniment.
  • Si G0 rencontre un appel système, M0 est suspendu, libérant P. M1 peut alors s'attacher à P et continuer l'exécution des autres goroutines.

Si G0 rencontre un appel système, M0 est suspendu, libérant P. M1 peut alors s'attacher à P et continuer l'exécution des autres goroutines. Une fois que M1 a terminé ou est également suspendu, P est relâché et peut être repris par M0.

Dans un système à plusieurs cœurs logiques, plusieurs P sont créés, permettant l'exécution parallèle de plusieurs threads. Si les files d'attente locales (LRQ) sont déséquilibrées, un P peut "voler" des G d'autres LRQ, généralement en prenant la moitié.

Package runtime

runtime.GOMAXPROCS

runtime.GOMAXPROCS() permet de définir le nombre de P. Il est généralement configuré pour correspondre au nombre de cœurs logiques du CPU :

fmt.Println(runtime.NumCPU())
runtime.GOMAXPROCS(runtime.NumCPU()) // Utilise tous les cœurs logiques du CPU

// Résultat
Sur un système à 16 cœurs et 24 threads, 24 P seront utilisés.

runtime.Gosched

runtime.Gosched() permet à une goroutine de céder son temps d'exécution, permettant à d'autres goroutines d'être exécutées :

func main() {
	runtime.GOMAXPROCS(1)
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("goroutine")
		}
	}()

	runtime.Gosched()
	fmt.Println("main")
}

// Résultat possible
goroutine
goroutine
goroutine
goroutine
goroutine
main

L'ordre d'impression peut varier, car la goroutine principale cède son temps d'exécution.

runtime.Goexit

runtime.Goexit() termine la goroutine en cours, mais exécute les instructions defer. Cette fonction ne doit pas être utilisée dans la fonction main, car elle provoque une panique :

func main() {
	runtime.GOMAXPROCS(1)
	go func() {
		defer fmt.Println("defer exécuté")
		fmt.Println("je suis exécuté")
		runtime.Goexit()
		fmt.Println("je suis sauté")
	}()

	time.Sleep(1 * time.Second)
}

// Résultat
je suis exécuté
defer exécuté

Étiquettes: Go Goroutines Runtime GMP Planification

Publié le 1 octobre à 03h19