109 votes

Nombre maximal de goroutines

Combien de goroutines puis-je utiliser sans douleur? Par exemple, Wikipédia dit qu'en Erlang, 20 millions de processus peuvent être créés sans dégradation des performances.

Mise à jour: Je viens de faire des recherches sur les performances des goroutines un peu et j'ai obtenu ces résultats :

  • Il semble que la durée de vie d'une goroutine est plus longue que le calcul de sqrt() 1000 fois (~45µs pour moi), la seule limitation est la mémoire
  • Une goroutine coûte 4 à 4,5 Ko

101voto

Si une goroutine est bloquée, il n'y a aucun coût impliqué autre que :

  • utilisation de la mémoire
  • garbage-collection plus lent

Les coûts (en termes de mémoire et de temps moyen pour réellement commencer à exécuter une goroutine) sont :

Go 1.6.2 (Avril 2016)
  CPU 32 bits x86 (A10-7850K 4GHz)
    | Nombre de goroutines : 100000
    | Par goroutine :
    |   Mémoire : 4536,84 octets
    |   Temps : 1,634248 µs
  CPU 64 bits x86 (A10-7850K 4GHz)
    | Nombre de goroutines : 100000
    | Par goroutine :
    |   Mémoire : 4707,92 octets
    |   Temps : 1,842097 µs

Go release.r60.3 (Décembre 2011)
  CPU 32 bits x86 (1,6 GHz)
    | Nombre de goroutines : 100000
    | Par goroutine :
    |   Mémoire : 4243,45 octets
    |   Temps : 5,815950 µs

Sur une machine avec 4 Go de mémoire installée, cela limite le nombre maximal de goroutines à légèrement moins de 1 million.


Code source (pas besoin de le lire si vous comprenez déjà les chiffres imprimés ci-dessus) :

package main

import (
    "flag"
    "fmt"
    "os"
    "runtime"
    "time"
)

var n = flag.Int("n", 1e5, "Nombre de goroutines à créer")

var ch = make(chan byte)
var counter = 0

func f() {
    counter++
    <-ch // Bloquer cette goroutine
}

func main() {
    flag.Parse()
    if *n <= 0 {
            fmt.Fprintf(os.Stderr, "nombre invalide de goroutines")
            os.Exit(1)
    }

    // Limiter le nombre de threads OS de réserve à 1 seulement
    runtime.GOMAXPROCS(1)

    // Faire une copie de MemStats
    var m0 runtime.MemStats
    runtime.ReadMemStats(&m0)

    t0 := time.Now().UnixNano()
    for i := 0; i < *n; i++ {
            go f()
    }
    runtime.Gosched()
    t1 := time.Now().UnixNano()
    runtime.GC()

    // Faire une copie de MemStats
    var m1 runtime.MemStats
    runtime.ReadMemStats(&m1)

    if counter != *n {
            fmt.Fprintf(os.Stderr, "échec pour commencer l'exécution de toutes les goroutines")
            os.Exit(1)
    }

    fmt.Printf("Nombre de goroutines : %d\n", *n)
    fmt.Printf("Par goroutine :\n")
    fmt.Printf("  Mémoire : %.2f octets\n", float64(m1.Sys-m0.Sys)/float64(*n))
    fmt.Printf("  Temps :   %f µs\n", float64(t1-t0)/float64(*n)/1e3)
}

34voto

Nils von Barth Points 121

Des centaines de milliers, selon la FAQ Go : Pourquoi les goroutines au lieu des threads?:

Il est pratique de créer des centaines de milliers de goroutines dans le même espace d'adressage.

Le test test/chan/goroutines.go crée 10 000 et pourrait facilement en faire plus, mais est conçu pour s'exécuter rapidement ; vous pouvez changer le nombre sur votre système pour expérimenter. Vous pouvez facilement exécuter des millions, en fonction de la mémoire disponible, comme sur un serveur.

Pour comprendre le nombre maximal de goroutines, notez que le coût par goroutine est principalement la pile. Selon la FAQ encore une fois :

…les goroutines peuvent être très bon marché : elles n'ont que peu de frais généraux en dehors de la mémoire pour la pile, qui ne fait que quelques kilooctets.

Un calcul approximatif consiste à supposer qu'une goroutine a une page de 4 Kio allouée pour la pile (4 Kio est une taille assez uniforme), plus certains petits frais généraux pour un bloc de contrôle (comme un Bloc de Contrôle de Thread) pour le runtime ; cela correspond à ce que vous avez observé (en 2011, avant Go 1.0). Ainsi, 100 Ki routines prendraient environ 400 MiB de mémoire, et 1 Mi routines prendraient environ 4 GiB de mémoire, ce qui est toujours gérable sur un ordinateur de bureau, un peu trop pour un téléphone, et très gérable sur un serveur. En pratique, la taille de la pile de départ varie de la demi-page (2 KiB) à deux pages (8 KiB), donc c'est approximativement correct.

La taille de la pile de départ a changé avec le temps ; elle a commencé à 4 KiB (une page), puis elle a été portée à 8 KiB (2 pages) dans 1.2, puis diminuée à 2 KiB (demi-page) dans 1.4. Ces changements étaient dus à des piles segmentées provoquant des problèmes de performance lors de basculements rapides entre les segments ("hot stack split"), augmentées pour atténuer (1.2), puis diminuées lorsque les piles segmentées ont été remplacées par des piles contiguës (1.4) :

Notes de publication de Go 1.2 : Taille de la pile:

Dans Go 1.2, la taille minimale de la pile lorsqu'une goroutine est créée est passée de 4k à 8k.

Notes de publication de Go 1.4 : Changements dans le runtime:

la taille de départ par défaut pour la pile d'une goroutine en 1.4 a été réduite de 8192 octets à 2048 octets.

La mémoire par goroutine est principalement la pile, et elle commence bas et augmente afin que vous puissiez avoir beaucoup de goroutines à moindre coût. Vous pourriez utiliser une pile de départ plus petite, mais alors elle devrait croître plus tôt (gagner de l'espace au détriment du temps), et les avantages diminuent en raison du bloc de contrôle qui ne rétrécit pas. Il est possible d'éliminer la pile, du moins lorsqu'elle est échangée (par exemple, faire toute l'allocation sur le tas, ou sauvegarder la pile sur le tas lors d'un changement de contexte), mais cela nuit aux performances et ajoute de la complexité. C'est possible (comme dans Erlang), et cela signifie que vous auriez juste besoin du bloc de contrôle et du contexte sauvegardé, permettant un facteur supplémentaire de 5×–10× dans le nombre de goroutines, limité maintenant par la taille du bloc de contrôle et la taille sur le tas des variables locales de la goroutine. Cependant, cela n'est pas très utile, sauf si vous avez besoin de millions de petites goroutines en sommeil.

Comme l'utilisation principale de nombreuses goroutines est pour les tâches liées à l'IO (concrètement pour traiter les appels système bloquants, notamment l'IO réseau ou système de fichiers), vous avez beaucoup plus de chances de rencontrer des limites du système d'exploitation sur d'autres ressources, à savoir les sockets réseau ou les descripteurs de fichiers : golang-nuts › Le nombre maximal de goroutines et de descripteurs de fichiers?. La manière habituelle de traiter cela est avec une piscine de la ressource rare, ou plus simplement en limitant le nombre via un sémaphore; voir Conservation des Descripteurs de Fichiers en Go et Limitation de la Concurrence en Go.

9voto

peterSO Points 25725

Pour paraphraser, il y a des mensonges, des sacrés mensonges, et des benchmarks. Comme l'a avoué l'auteur du benchmark Erlang,

Il va sans dire qu'il n'y avait pas assez de mémoire disponible dans la machine pour effectuer réellement quelque chose d'utile. test de stress erlang

Quel est votre matériel, quel est votre système d'exploitation, où se trouve le code source de votre benchmark? Que cherche à mesurer et prouver/réfuter ce benchmark?

8voto

jimt Points 7028

Cela dépend entièrement du système sur lequel vous exécutez. Mais les goroutines sont très légères. Un processus moyen ne devrait pas avoir de problèmes avec 100 000 routines concurrentes. Bien sûr, nous ne pouvons pas répondre à cette question sans savoir quelle est la plateforme cible.

2voto

Travis R Points 8935

Voici un excellent article de Dave Cheney sur ce sujet : http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite

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