web-dev-qa-db-fra.com

Nombre maximum de goroutines

Combien de goroutines puis-je utiliser sans douleur? Par exemple, selon wikipedia, dans Erlang, 20 millions de processus peuvent être créés sans dégrader les performances.

Mise à jour: Je viens de enquêté sur les performances des goroutines un peu et j'ai obtenu un tel résultat:

  • Il semble que la durée de vie de goroutine soit plus que de calculer sqrt () 1000 fois (~ 45µs pour moi), la seule limitation est la mémoire
  • Goroutine coûte 4 - 4,5 Ko
59
OCyril

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

  • utilisation de la mémoire
  • ramassage des ordures plus lent

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

Go 1.6.2 (April 2016)
  32-bit x86 CPU (A10-7850K 4GHz)
    | Number of goroutines: 100000
    | Per goroutine:
    |   Memory: 4536.84 bytes
    |   Time:   1.634248 µs
  64-bit x86 CPU (A10-7850K 4GHz)
    | Number of goroutines: 100000
    | Per goroutine:
    |   Memory: 4707.92 bytes
    |   Time:   1.842097 µs

Go release.r60.3 (December 2011)
  32-bit x86 CPU (1.6 GHz)
    | Number of goroutines: 100000
    | Per goroutine:
    |   Memory: 4243.45 bytes
    |   Time:   5.815950 µs

Sur une machine avec 4 Go de mémoire installée, cela limite le nombre maximum de goroutines à un peu moins d'un 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, "Number of goroutines to create")

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

func f() {
    counter++
    <-ch // Block this goroutine
}

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

    // Limit the number of spare OS threads to just 1
    runtime.GOMAXPROCS(1)

    // Make a copy of 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()

    // Make a copy of MemStats
    var m1 runtime.MemStats
    runtime.ReadMemStats(&m1)

    if counter != *n {
            fmt.Fprintf(os.Stderr, "failed to begin execution of all goroutines")
            os.Exit(1)
    }

    fmt.Printf("Number of goroutines: %d\n", *n)
    fmt.Printf("Per goroutine:\n")
    fmt.Printf("  Memory: %.2f bytes\n", float64(m1.Sys-m0.Sys)/float64(*n))
    fmt.Printf("  Time:   %f µs\n", float64(t1-t0)/float64(*n)/1e3)
}
47
user811773

Des centaines de milliers, par Go FAQ: Pourquoi des goroutines au lieu de fils? :

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 faire plus, mais est conçu pour fonctionner rapidement; vous pouvez modifier le nombre sur votre système pour expérimenter. Vous pouvez facilement exécuter des millions, avec suffisamment de mémoire, comme sur un serveur.

Pour comprendre le nombre maximum de goroutines, notez que le coût par goroutine est principalement la pile. Par FAQ encore:

… Les goroutines, peuvent être très bon marché: elles ont peu de surcharge au-delà de la mémoire de la pile, qui n'est que de quelques kilo-octets.

Un calcul au dos de l'enveloppe consiste à supposer que chaque goroutine a un 4 Ko page alloué à la pile (4 Ko est une taille assez uniforme), plus quelques petits frais généraux pour un contrôle bloc (comme un Thread Control Block ) pour le runtime; cela correspond à ce que vous avez observé (en 2011, pré-Go 1.0). Ainsi, 100 Ki routines prendraient environ 400 Mo de mémoire, et 1 Mi routines prendraient environ 4 GiB de mémoire, qui est toujours gérable sur le bureau, un peu pour un téléphone, et très gérable sur un serveur. En pratique, la pile de départ a varié d'une demi-page (2 Ko) à deux pages (8 Ko), donc c'est à peu près correct.

La taille de la pile de départ a changé au fil du temps; il a commencé à 4 Ko (une page), puis en 1,2 a été augmenté à 8 Ko (2 pages), puis en 1,4 a été réduit à 2 Ko (une demi-page). Ces changements étaient dus à des piles segmentées entraînant des problèmes de performances lors d'un basculement rapide entre les segments ("partage de pile à chaud"), donc augmentées pour atténuer (1.2), puis diminuées lorsque les piles segmentées étaient remplacées par des piles contiguës (1.4):

Notes de mise à jour de Go 1.2: Taille de la pile :

Dans Go 1.2, la taille minimale de la pile lors de la création d'un goroutine est passée de 4 Ko à 8 Ko

Go 1.4 Release Notes: Modifications du runtime :

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

La mémoire par goroutine est en grande partie empilée, et elle commence bas et augmente donc vous pouvez avoir à bon marché de nombreux goroutines. Vous pouvez utiliser une pile de départ plus petite, mais elle devra alors croître plus tôt (gagner de l'espace au prix du temps) et les avantages diminueront car le bloc de contrôle ne diminuera pas. Il est possible d'éliminer la pile, au moins lors de l'échange (par exemple, faire toute l'allocation sur le tas ou enregistrer la pile dans le tas lors du changement de contexte), bien que cela nuise aux performances et ajoute de la complexité. C'est possible (comme dans Erlang), et signifie que vous auriez juste besoin du bloc de contrôle et du contexte enregistré, permettant un autre facteur de 5 × –10 × en nombre de goroutines, limité maintenant par la taille du bloc de contrôle et la taille sur le tas de la goroutine -les variables locales. Cependant, ce n'est pas très utile, sauf si vous avez besoin de millions de minuscules goroutines endormies.

Étant donné que l'utilisation principale de nombreux goroutines est destinée aux tâches liées aux E/S (concrètement pour traiter les appels système bloquants, notamment les E/S du réseau ou du système de fichiers), vous êtes beaucoup plus susceptible de rencontrer des limites de système d'exploitation sur d'autres ressources, à savoir les sockets réseau ou les descripteurs de fichiers. : golang-nuts ›Le nombre maximum de goroutines et de descripteurs de fichiers? . La manière habituelle de résoudre ce problème est d'utiliser un pool de la ressource rare, ou plus simplement en limitant simplement le nombre via un sémaphore ; voir Conservation des descripteurs de fichiers dans Go et Limitation de la concurrence dans Go .

15
Nils von Barth

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 avoir aucun problème avec 100 000 routines simultanées. Que cela se passe pour votre plate-forme cible est, bien sûr, une question à laquelle nous ne pouvons pas répondre sans savoir ce qu'est cette plate-forme.

5
jimt

Pour paraphraser, il y a des mensonges, des putains de mensonges et des repères. Comme l'a avoué l'auteur de la référence Erlang,

Il va sans dire qu'il n'y avait pas assez de mémoire dans la machine pour faire quoi que ce soit d'utile. test d'effort erlang

Quel est votre matériel, quel est votre système d'exploitation, où est votre code source de référence? Quelle est la référence essayant de mesurer et de prouver/réfuter?

5
peterSO

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

2
Travis Reeder

Si le nombre de goroutine devient un problème, vous pouvez facilement le limiter pour votre programme:
Voir ( mr51m0n/gorc et cet exemple .

Définir des seuils sur le nombre de goroutines en cours d'exécution

Peut augmenter et diminuer un compteur lors du démarrage ou de l'arrêt d'un goroutine.
Il peut attendre un nombre minimum ou maximum de goroutines en cours d'exécution, permettant ainsi de définir des seuils pour le nombre de goroutines gouvernées par gorc qui s'exécutent en même temps.

0
VonC