43 votes

Comprendre les goroutines

J'essaie de comprendre la concurrence en Go. En particulier, j'ai écrit ce programme non sécurisé par les threads :

package main

import "fmt"

var x = 1

func inc_x() { //test
  for {
    x += 1
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

Je reconnais que je devrais utiliser des canaux pour empêcher les conditions de course avec x mais ce n'est pas le sujet ici. Le programme imprime 1 et semble ensuite tourner en boucle à l'infini (sans rien imprimer de plus). Je m'attendrais à ce qu'il imprime une liste infinie de nombres, en sautant peut-être certains et en répétant d'autres à cause de la condition de course (ou pire -- en imprimant le nombre pendant qu'il est mis à jour dans le fichier de configuration de l'ordinateur). inc_x ).

Ma question est la suivante : pourquoi le programme n'imprime-t-il qu'une seule ligne ?

Soyons clairs : je n'utilise pas les canaux à dessein pour cet exemple de jouet.

42voto

Jeremy Wall Points 10643

Il y a quelques points à garder à l'esprit à propos des Go's goroutines :

  1. Il ne s'agit pas de threads au sens des threads de Java ou de C++.
    • Les goroutines ressemblent plus à verdures
  2. Le runtime de go multiplexe les goroutines à travers le fils du système
    • le nombre de threads système est contrôlé par une variable d'environnement GOMAXPROCS et la valeur par défaut est de 1 actuellement je pense. Cela peut changer à l'avenir
  3. La façon dont les goroutines retournent à leur thread actuel est contrôlée par plusieurs constructions différentes
    • el sélectionnez l'énoncé peut rendre le contrôle au fil
    • l'envoi sur un canal peut rendre le contrôle au fil
    • en effectuant des opérations d'E/S, le contrôle peut revenir au thread
    • runtime.Gosched() rend explicitement le contrôle au fil d'exécution

Le comportement que vous observez est dû au fait que la fonction principale ne revient jamais au thread et qu'elle est impliquée dans une boucle occupée. Comme il n'y a qu'un seul thread, la boucle principale n'a pas de place pour s'exécuter.

0 votes

Je pensais juste mentionner que depuis Go 1.2, l'ordonnanceur peut être invoqué à l'entrée d'une fonction de façon périodique. Cela ne résoudrait pas ce cas, je ne pense pas, mais cela aide lorsque vous avez une boucle serrée qui appelle une fonction non-inlined.

18voto

lunixbochs Points 7475

Según este y este Certains appels ne peuvent pas être invoqués pendant une Goroutine liée au CPU (si la Goroutine ne cède jamais au planificateur). Cela peut entraîner le blocage d'autres goroutines si elles doivent bloquer le thread principal (comme c'est le cas avec la fonction write() syscall utilisé par fmt.Println() )

La solution que j'ai trouvée consiste à appeler runtime.Gosched() dans votre thread lié au processeur pour le rendre au planificateur, comme suit :

package main

import (
  "fmt"
  "runtime"
)

var x = 1

func inc_x() {
  for {
    x += 1
    runtime.Gosched()
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

Parce que vous n'effectuez qu'une seule opération dans la Goroutine, runtime.Gosched() est appelé très souvent. Appeler runtime.GOMAXPROCS(2) on init est plus rapide d'un ordre de grandeur, mais serait très peu sûr si vous faisiez quelque chose de plus compliqué que d'incrémenter un nombre (par exemple, traiter des tableaux, des structs, des maps, etc).

Dans ce cas, la meilleure pratique serait potentiellement d'utiliser un canal pour gérer l'accès partagé à une ressource.

Mise à jour : à partir de Go 1.2, tout appel de fonction non souligné peut invoquer le planificateur.

3 votes

Est-il exact de dire que ce problème ne se produit jamais avec une utilisation correcte des canaux ? Sinon, cela semble être un problème majeur avec Go - si un thread peut monopoliser le CPU et qu'aucun autre thread ne peut s'exécuter.

0 votes

Si vous avez plusieurs processeurs, vous pouvez également définir la variable d'environnement GOMAXPROCS=2 pour que la goroutine s'exécute dans un thread distinct de la fonction principale. La fonction Gosched() indique au runtime de céder dans cette boucle for.

0 votes

@user793587 Cela dépend de ce que vous entendez par "correct". L'interrogation dans une boucle serrée peut monopoliser un thread, mais le code est mauvais de toute façon. En pratique, ce n'est tout simplement pas un problème. Dans le cas rare où vous besoin de pour interroger dans une boucle serrée, vous pouvez explicitement céder au planificateur, mais cela n'apparaît normalement que dans les exemples de jouets. J'ai entendu parler de plans pour passer à un ordonnanceur préemptif, mais l'ordonnanceur coopératif actuel fonctionne bien dans la plupart des cas.

8voto

Sonia Points 6077

C'est une interaction de deux choses. Premièrement, par défaut, Go n'utilise qu'un seul cœur, et deuxièmement, Go doit planifier les goroutines de manière coopérative. Votre fonction inc_x n'a pas de rendement et monopolise donc l'unique cœur utilisé. En supprimant l'une ou l'autre de ces conditions, vous obtiendrez le résultat que vous attendez.

Dire "noyau" est un peu exagéré. Go peut en fait utiliser plusieurs cœurs en coulisse, mais il utilise une variable appelée GOMAXPROCS pour déterminer le nombre de threads à programmer pour vos goroutines qui effectuent des tâches non liées au système. Comme expliqué dans le FAQ y Go efficace la valeur par défaut est 1, mais elle peut être fixée plus haut avec une variable d'environnement ou une fonction d'exécution. Cela donnera probablement le résultat que vous attendez, mais seulement si votre processeur a plusieurs cœurs.

Indépendamment des cœurs et des GOMAXPROCS, vous pouvez donner au planificateur de goroutine dans le runtime une chance de faire son travail. Le planificateur ne peut pas préempter une goroutine en cours d'exécution, mais doit attendre qu'elle revienne vers le runtime et demande un service, comme IO, time.Sleep() ou runtime.Gosched(). L'ajout d'un tel élément dans inc_x produit un résultat attendu. La goroutine qui exécute main() demande déjà un service avec fmt.Println, donc avec les deux goroutines qui cèdent maintenant périodiquement au runtime, il peut faire une sorte d'ordonnancement équitable.

0 votes

Vous pouvez exécuter plusieurs fils/processus simultanés sur un seul processeur, donc "seulement si votre processeur a plusieurs cœurs" est trompeur.

3voto

lazy1 Points 3691

Pas sûr, mais je pensez à que inc_x monopolise l'unité centrale. Comme il n'y a pas d'entrée-sortie, il ne libère pas le contrôle.

J'ai trouvé deux choses qui ont résolu ce problème. La première était d'appeler runtime.GOMAXPROCS(2) au début du programme et alors ça marchera puisque maintenant il y a deux threads servant des goroutings. L'autre solution consiste à insérer time.Sleep(1) après avoir incrémenté x .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X