Maximale Anzahl Goroutinen

71

Wie viele Goroutinen kann ich schmerzfrei verwenden? Zum Beispiel sagt Wikipedia, dass in Erlang 20 Millionen Prozesse erstellt werden können, ohne die Leistung zu beeinträchtigen.

Update: Ich habe gerade ein wenig die Leistung von Goroutinen untersucht und solche Ergebnisse erzielt:

  • Es sieht so aus, als ob die Lebensdauer der Goroutine mehr ist als die 1000-fache Berechnung von sqrt () (~ 45µs für mich). Die einzige Einschränkung ist der Speicher
  • Goroutine kostet 4 - 4,5 KB
OCyril
quelle

Antworten:

63

Wenn eine Goroutine blockiert ist, fallen keine anderen Kosten an als:

  • Speichernutzung
  • langsamere Müllabfuhr

Die Kosten (in Bezug auf Speicher und durchschnittliche Zeit, um tatsächlich mit der Ausführung einer Goroutine zu beginnen) sind:

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

Auf einem Computer mit 4 GB installiertem Speicher wird die maximale Anzahl von Goroutinen auf etwas weniger als 1 Million begrenzt.


Quellcode (Sie müssen diesen nicht lesen, wenn Sie die oben angegebenen Zahlen bereits verstanden haben):

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)
}

quelle
2
Ihre Konvertierung von ~ 4k / pro Goroutine (dies hat sich von Release zu Release geändert; und Sie müssen auch die Verwendung des Goroutine-Stacks berücksichtigen) auf ein Maximum basierend auf dem installierten Speicher ist fehlerhaft. Das Maximum basiert auf dem kleineren des adressierbaren virtuellen Speichers (normalerweise 2-3 GB für 32-Bit-Betriebssysteme) oder dem physischen Speicher plus verfügbarem Swap-Speicher oder den Speicherressourcengrenzen des Prozesses (häufig unbegrenzt). Beispielsweise ist auf einem 64-Bit-Computer mit vernünftigem Swap-Setup der installierte physische Speicher für keine Begrenzung irrelevant (die Leistung nimmt jedoch ab, wenn der Swap beginnt).
Dave C
Ich denke, dies enthält eine Rennbedingung, da es keine explizite Synchronisation gibt, um sicherzustellen, dass alle Goroutinen gestartet wurden, bevor der Zähler mit verglichen wird n. Hast du jedes Mal Glück? :)
Filip Haglund
2
Der Go-Spielplatz meldet 2758.41 bytespro Goroutine, läuft go 1.5.1.
Filip Haglund
1
Wie @FilipHaglund feststellt, haben sich die Zahlen im Laufe der Zeit geändert. Dies ist hauptsächlich auf die Änderung der Startstapelgröße zurückzuführen (4 KiB, dann 8 KiB in 1,2, dann 2 KiB in 1,4).
Nils von Barth
Er hat die Mathematik
Bürger Conn
20

Hunderttausende pro Go FAQ: Warum Goroutinen statt Threads? ::

Es ist praktisch, Hunderttausende von Goroutinen im selben Adressraum zu erstellen.

Der Test test / chan / goroutines.go erstellt 10.000 und könnte leicht mehr tun, ist jedoch so konzipiert, dass er schnell ausgeführt wird. Sie können die Nummer auf Ihrem System ändern, um zu experimentieren. Sie können problemlos Millionen ausführen, wenn genügend Speicher vorhanden ist, z. B. auf einem Server.

Um die maximale Anzahl von Goroutinen zu verstehen, beachten Sie, dass die Kosten pro Goroutine in erster Linie der Stapel sind. Nochmals per FAQ:

… Goroutinen können sehr billig sein: Sie haben nur wenig Overhead außerhalb des Speichers für den Stapel, der nur wenige Kilobyte beträgt.

Bei einer Back-of-the-Envelope-Berechnung wird davon ausgegangen, dass jeder Goroutine eine 4-KiB- Seite für den Stapel zugewiesen ist (4 KiB ist eine ziemlich einheitliche Größe), plus ein wenig Overhead für einen Steuerblock (wie einen Thread- Steuerblock ) für die Laufzeit; Dies stimmt mit dem überein, was Sie beobachtet haben (2011 vor Go 1.0). Somit würden 100 Ki-Routinen ungefähr 400 MiB Speicher benötigen, und 1 Mi-Routinen würden ungefähr 4 GiB Speicher benötigen, was auf dem Desktop immer noch verwaltbar ist, ein bisschen viel für ein Telefon und auf einem Server sehr verwaltbar. In der Praxis hat der Startstapel eine Größe von einer halben Seite (2 KiB) bis zwei Seiten (8 KiB), was ungefähr korrekt ist.

Die Größe des Startstapels hat sich im Laufe der Zeit geändert. es begann bei 4 KiB (eine Seite), dann wurde in 1.2 auf 8 KiB (2 Seiten) erhöht, dann wurde in 1.4 auf 2 KiB (eine halbe Seite) verringert. Diese Änderungen waren auf segmentierte Stapel zurückzuführen, die Leistungsprobleme beim schnellen Hin- und Herwechseln zwischen Segmenten verursachten ("Hot-Stack-Split"), also zur Minderung erhöht (1.2) und dann verringert, wenn segmentierte Stapel durch zusammenhängende Stapel ersetzt wurden (1.4):

Go 1.2 Versionshinweise: Stapelgröße :

In Go 1.2 wurde die Mindestgröße des Stapels beim Erstellen einer Goroutine von 4 KB auf 8 KB angehoben

Go 1.4 Versionshinweise: Änderungen an der Laufzeit :

Die Standardstartgröße für den Stapel einer Goroutine in 1.4 wurde von 8192 Byte auf 2048 Byte reduziert.

Der Speicher pro Goroutine ist größtenteils gestapelt, beginnt niedrig und wächst, sodass Sie kostengünstig viele Goroutinen haben können. Sie könnten einen kleineren Startstapel verwenden, aber dann müsste er früher wachsen (Speicherplatz auf Kosten der Zeit gewinnen), und die Vorteile nehmen ab, da der Steuerblock nicht schrumpft. Es ist möglich, den Stapel zu entfernen, zumindest wenn er ausgetauscht wird (z. B. alle Zuordnungen auf dem Heap vornehmen oder den Stapel auf dem Heap beim Kontextwechsel speichern), obwohl dies die Leistung beeinträchtigt und die Komplexität erhöht. Dies ist möglich (wie in Erlang) und bedeutet, dass Sie nur den Kontrollblock und den gespeicherten Kontext benötigen. Dies ermöglicht einen weiteren Faktor von 5 × –10 × für die Anzahl der Goroutinen, der jetzt durch die Größe des Kontrollblocks und die Größe der Goroutine auf dem Heap begrenzt ist -lokale Variablen. Dies ist jedoch nicht besonders nützlich, es sei denn, Sie benötigen Millionen winziger schlafender Goroutinen.

Da viele Goroutinen hauptsächlich für E / A-gebundene Aufgaben verwendet werden (konkret zur Verarbeitung blockierender Systemaufrufe, insbesondere von Netzwerk- oder Dateisystem-E / A), stoßen Sie viel häufiger auf Betriebssystembeschränkungen für andere Ressourcen, nämlich Netzwerksockets oder Dateihandles : Golang-Nüsse ›Die maximale Anzahl von Goroutinen und Dateideskriptoren? . Der übliche Weg, dies zu beheben, besteht darin, einen Pool der knappen Ressourcen zu erstellen oder einfach die Anzahl über ein Semaphor zu begrenzen . Siehe Beibehalten von Dateideskriptoren in Go und Begrenzen der Parallelität in Go .

Nils von Barth
quelle
1
Die Begrenzung der Parallelität in go ist ein sehr schönes und einfaches Beispiel
gabuzo
6

Das hängt ganz von dem System ab, auf dem Sie laufen. Aber Goroutinen sind sehr leicht. Ein durchschnittlicher Prozess sollte keine Probleme mit 100.000 gleichzeitigen Routinen haben. Ob dies für Ihre Zielplattform gilt, können wir natürlich nicht beantworten, ohne zu wissen, um welche Plattform es sich handelt.

jimt
quelle
Hatten Sie keine Probleme mit einem ARM-basierten Tablet?
PeterSO
1
Da ich kein ARM-basiertes Tablet habe, kann ich nicht sagen. Der Punkt bleibt jedoch bestehen. Es ist unmöglich zu sagen, ohne zu wissen, was das Zielsystem tun kann.
Jimt
1
Mit anderen Worten, Ihre Behauptung "keine Probleme mit 100.000 gleichzeitigen Routinen" ist ohne richtigen Kontext bedeutungslos.
PeterSO
4
Sie nehmen es aus dem Zusammenhang. Der Satz lautet "Ein durchschnittlicher Prozess sollte keine Probleme mit 100.000 gleichzeitigen Routinen haben".
Jimt
5

Um es zu paraphrasieren, es gibt Lügen, verdammte Lügen und Maßstäbe. Wie der Autor des Erlang-Benchmarks gestand,

Es versteht sich von selbst, dass nicht genügend Speicher in der Maschine vorhanden war, um tatsächlich etwas Nützliches zu tun. Stresstest erlang

Was ist Ihre Hardware, was ist Ihr Betriebssystem, wo ist Ihr Benchmark-Quellcode? Was versucht der Benchmark zu messen und zu beweisen / zu widerlegen?

peterSO
quelle
0

Wenn die Anzahl der Goroutinen jemals zu einem Problem wird, können Sie sie für Ihr Programm leicht einschränken:
Siehe mr51m0n / gorc und dieses Beispiel .

Legen Sie Schwellenwerte für die Anzahl der laufenden Goroutinen fest

Kann einen Zähler erhöhen und verringern, wenn eine Goroutine gestartet oder gestoppt wird.
Es kann warten, bis eine minimale oder maximale Anzahl von Goroutinen ausgeführt wird, wodurch Schwellenwerte für die Anzahl der gleichzeitig ausgeführten gorcgeregelten Goroutinen festgelegt werden können.

VonC
quelle