Que fait exactement runtime.Gosched?

86

Dans une version antérieure à la sortie de go 1.5 du site Web Tour of Go , il y a un morceau de code qui ressemble à ceci.

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

La sortie ressemble à ceci:

hello
world
hello
world
hello
world
hello
world
hello

Ce qui me dérange, c'est que lorsqu'il runtime.Gosched()est supprimé, le programme n'imprime plus "monde".

hello
hello
hello
hello
hello

Pourquoi est-ce si? Comment cela runtime.Gosched()affecte-t-il l'exécution?

Jason Yeo
la source

Réponses:

143

Remarque:

À partir de Go 1.5, GOMAXPROCS est défini sur le nombre de cœurs du matériel: golang.org/doc/go1.5#runtime , en dessous de la réponse d'origine avant 1.5.


Lorsque vous exécutez le programme Go sans spécifier la variable d'environnement GOMAXPROCS, les routines Go sont planifiées pour une exécution dans un seul thread OS. Cependant, pour donner l'impression que le programme est multithread (c'est à cela que servent les goroutines, n'est-ce pas?), Le planificateur Go doit parfois changer de contexte d'exécution, afin que chaque goroutine puisse faire son travail.

Comme je l'ai dit, lorsque la variable GOMAXPROCS n'est pas spécifiée, le runtime Go n'est autorisé à utiliser qu'un seul thread, il est donc impossible de changer de contexte d'exécution pendant que goroutine effectue un travail conventionnel, comme des calculs ou même IO (qui est mappé à des fonctions C simples ). Le contexte ne peut être changé que lorsque les primitives de concurrence Go sont utilisées, par exemple lorsque vous activez plusieurs canaux, ou (c'est votre cas) lorsque vous dites explicitement au planificateur de changer les contextes - c'est à cela que runtime.Goschedsert.

Donc, en bref, lorsque le contexte d'exécution dans un goroutine atteint l' Goschedappel, le planificateur est chargé de basculer l'exécution sur un autre goroutine. Dans votre cas, il y a deux goroutines, main (qui représente le thread «principal» du programme) et supplémentaire, celle avec laquelle vous avez créé go say. Si vous supprimez l' Goschedappel, le contexte d'exécution ne sera jamais transféré de la première goroutine à la seconde, donc pas de «monde» pour vous. Quand Goschedest présent, l'ordonnanceur transfère l'exécution à chaque itération de boucle de la première goroutine à la seconde et vice versa, donc vous avez entrelacé «bonjour» et «monde».

Pour info, cela s'appelle le «multitâche coopératif»: les goroutines doivent explicitement céder le contrôle aux autres goroutines. L'approche utilisée dans la plupart des systèmes d'exploitation contemporains est appelée «multitâche préemptif»: les threads d'exécution ne sont pas concernés par le transfert de contrôle; le planificateur change les contextes d'exécution de manière transparente vers eux à la place. L'approche coopérative est fréquemment utilisée pour implémenter des `` threads verts '', c'est-à-dire des coroutines logiques concurrentes qui ne mappent pas 1: 1 aux threads du système d'exploitation - c'est ainsi que le runtime Go et ses goroutines sont implémentés.

Mise à jour

J'ai mentionné la variable d'environnement GOMAXPROCS mais je n'ai pas expliqué de quoi il s'agissait. Il est temps de résoudre ce problème.

Lorsque cette variable est définie sur un nombre positif N, le runtime Go pourra créer jusqu'à Ndes threads natifs, sur lesquels tous les threads verts seront planifiés. Thread natif une sorte de thread qui est créé par le système d'exploitation (threads Windows, pthreads, etc.). Cela signifie que si Nest supérieur à 1, il est possible que les goroutines soient planifiées pour s'exécuter dans différents threads natifs et, par conséquent, s'exécuter en parallèle (au moins, à la hauteur des capacités de votre ordinateur: si votre système est basé sur un processeur multicœur, il est probable que ces threads seront vraiment parallèles; si votre processeur a un seul cœur, alors le multitâche préemptif implémenté dans les threads du système d'exploitation créera une visibilité de l'exécution parallèle).

Il est possible de définir la variable GOMAXPROCS en utilisant la runtime.GOMAXPROCS()fonction au lieu de prérégler la variable d' environnement. Utilisez quelque chose comme ceci dans votre programme au lieu du courant main:

func main() {
    runtime.GOMAXPROCS(2)
    go say("world")
    say("hello")
}

Dans ce cas, vous pouvez observer des résultats intéressants. Il est possible que vous obteniez des lignes «bonjour» et «monde» imprimées de manière inégale, par exemple

hello
hello
world
hello
world
world
...

Cela peut arriver si les goroutines sont planifiées pour séparer les threads du système d'exploitation. C'est en fait ainsi que fonctionne le multitâche préemptif (ou le traitement parallèle dans le cas des systèmes multicœurs): les threads sont parallèles et leur sortie combinée est indéterministe. BTW, vous pouvez quitter ou supprimer un Goschedappel, cela semble n'avoir aucun effet lorsque GOMAXPROCS est supérieur à 1.

Ce qui suit est ce que j'ai obtenu sur plusieurs exécutions du programme avec runtime.GOMAXPROCSappel.

hyperplex /tmp % go run test.go
hello
hello
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
hello
hello
hello
hello
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world

Voir, parfois la sortie est jolie, parfois non. Indéterminisme en action :)

Une autre mise à jour

On dirait que dans les nouvelles versions du compilateur Go Go runtime force les goroutines à céder non seulement sur l'utilisation des primitives de concurrence, mais aussi sur les appels système du système d'exploitation. Cela signifie que le contexte d'exécution peut être commuté entre les goroutines également sur les appels de fonctions IO. Par conséquent, dans les compilateurs Go récents, il est possible d'observer un comportement indéterministe même lorsque GOMAXPROCS n'est pas défini ou défini sur 1.

Vladimir Matveev
la source
Bon travail ! Mais je n'ai pas rencontré ce problème sous go 1.0.3, wierd.
WoooHaaaa
1
C'est vrai. Je viens de vérifier cela avec go 1.0.3, et oui, ce comportement n'apparaît pas: même avec GOMAXPROCS == 1 le programme fonctionnait comme si GOMAXPROCS> = 2. Il semble que dans la version 1.0.3, le planificateur a été modifié.
Vladimir Matveev
Je pense que les choses ont changé par rapport au compilateur 1.4. L'exemple dans la question OP semble être la création de threads OS alors que cela (-> gobyexample.com/atomic-counters ) semble créer une planification coopérative. Veuillez mettre à jour la réponse si cela est vrai
tez
8
À partir de Go 1.5, GOMAXPROCS est défini sur le nombre de cœurs du matériel: golang.org/doc/go1.5#runtime
thepanuto
1
@paulkon, que ce soit Gosched()nécessaire ou non dépend de votre programme, cela ne dépend pas de la GOMAXPROCSvaleur. L'efficacité du multitâche préventif par rapport au mode coopératif dépend également de votre programme. Si votre programme est lié aux E / S, alors le multitâche coopératif avec des E / S asynchrones sera probablement plus efficace (c'est-à-dire plus de débit) que les E / S basées sur des threads synchrones; si votre programme est lié au processeur (par exemple, de longs calculs), alors le multitâche coopératif sera beaucoup moins utile.
Vladimir Matveev
8

La planification coopérative est le coupable. Sans céder, l'autre goroutine (disons "monde") peut avoir légalement zéro chance de s'exécuter avant / quand main se termine, ce qui, selon les spécifications, met fin à toutes les gorutines - ie. l'ensemble du processus.

zzzz
la source
1
ok, donc runtime.Gosched()cède. Qu'est-ce que ça veut dire? Il ramène le contrôle à la fonction principale?
Jason Yeo
5
Dans ce cas précis, oui. Généralement, il demande à l'ordonnanceur de démarrer et d'exécuter n'importe laquelle des goroutines "prêtes" dans un ordre de sélection intentionnellement non spécifié.
zzzz