Wie viele Goroutinen kann Go ausführen? Ein tiefer Einblick in Ressourcenbeschränkungen
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Um zu verstehen, wie viele Goroutinen maximal erstellt werden können, müssen wir zuerst die folgenden Fragen klären:
- Was ist eine Goroutine?
- Welche Ressourcen verbraucht sie?
Was ist eine Goroutine?
Eine Goroutine ist ein leichtgewichtiger Thread, der von Go abstrahiert wird. Sie führt die Planung auf Anwendungsebene durch, wodurch wir problemlos nebenläufige Programmierung durchführen können.
Eine Goroutine kann mit dem Schlüsselwort go
gestartet werden. Der Compiler übersetzt dieses Schlüsselwort in einen runtime.newproc
-Funktionsaufruf mithilfe der Methoden cmd/compile/internal/gc.state.stmt
und cmd/compile/internal/gc.state.call
.
Beim Starten einer neuen Goroutine zur Ausführung einer Aufgabe verwendet sie runtime.newproc
, um ein g
zur Ausführung der Coroutine zu initialisieren.
Wie viele Ressourcen verbraucht eine Goroutine?
Speicherverbrauch
Durch das Starten und Blockieren von Goroutinen können wir Speicheränderungen vor und nach der Auswertung des Verbrauchs beobachten:
func getGoroutineMemConsume() { var c chan int var wg sync.WaitGroup const goroutineNum = 1000000 memConsumed := func() uint64 { runtime.GC() // GC auslösen, um Objektauswirkungen auszuschließen var memStat runtime.MemStats runtime.ReadMemStats(&memStat) return memStat.Sys } noop := func() { wg.Done() <-c // Verhindern, dass die Goroutine beendet und Speicher freigibt } wg.Add(goroutineNum) before := memConsumed() // Speicher vor dem Erstellen von Goroutinen for i := 0; i < goroutineNum; i++ { go noop() } wg.Wait() after := memConsumed() // Speicher nach dem Erstellen von Goroutinen fmt.Println(runtime.NumGoroutine()) fmt.Printf("%.3f KB bytes\n", float64(after-before)/goroutineNum/1024) }
Ergebnisanalyse:
Jede Goroutine verbraucht mindestens 2 KB Speicherplatz. Angenommen, ein Computer verfügt über 2 GB Speicher, dann beträgt die maximale Anzahl von Goroutinen, die gleichzeitig existieren können, 2 GB / 2 KB = 1 Million.
CPU-Verbrauch
Die Menge an CPU, die eine Goroutine verwendet, hängt stark von der Logik der Funktion ab, die sie ausführt. Wenn die Funktion CPU-intensive Berechnungen beinhaltet und über einen längeren Zeitraum ausgeführt wird, wird die CPU schnell zum Engpass.
Die Anzahl der Goroutinen, die gleichzeitig ausgeführt werden können, hängt davon ab, was das Programm tut. Wenn die Aufgaben speicherintensive Netzwerkoperationen sind, könnten bereits wenige Goroutinen das Programm zum Absturz bringen.
Fazit
Die Anzahl der Goroutinen, die ausgeführt werden können, hängt vom CPU- und Speicherverbrauch der darin ausgeführten Operationen ab. Wenn die Operationen minimal sind (d. h. nichts tun), wird der Speicher zuerst zum Engpass. In diesem Fall wird das Programm einen Fehler ausgeben, wenn 2 GB Speicher erschöpft sind. Wenn die Operationen CPU-intensiv sind, können bereits zwei oder drei Goroutinen dazu führen, dass das Programm fehlschlägt.
Häufige Probleme, die durch übermäßige Goroutinen ausgelöst werden
- zu viele offene Dateien – Dies tritt auf, weil zu viele Datei- oder Socket-Handles geöffnet sind.
- kein Speicher mehr
Anwendung in Geschäftsszenarien
Wie kann man die Anzahl der gleichzeitigen Goroutinen steuern?
runtime.NumGoroutine()
kann verwendet werden, um die Anzahl der aktiven Goroutinen zu überwachen.
1. Sicherstellen, dass nur eine Goroutine eine Aufgabe ausführt
Wenn in einer Schnittstelle Nebenläufigkeit erforderlich ist, sollte die Anzahl der Goroutinen auf Anwendungsebene verwaltet werden. Wenn beispielsweise eine Goroutine verwendet wird, um eine Ressource zu initialisieren, die nur einmal initialisiert werden muss, ist es unnötig, mehreren Goroutinen dies gleichzeitig zu erlauben. Ein running
-Flag kann verwendet werden, um festzustellen, ob die Initialisierung bereits läuft.
// SingerConcurrencyRunner stellt sicher, dass nur eine Aufgabe ausgeführt wird type SingerConcurrencyRunner struct { isRunning bool sync.Mutex } func NewSingerConcurrencyRunner() *SingerConcurrencyRunner { return &SingerConcurrencyRunner{} } func (c *SingerConcurrencyRunner) markRunning() (ok bool) { c.Lock() defer c.Unlock() // Doppelte Überprüfung, um Race Conditions zu vermeiden if c.isRunning { return false } c.isRunning = true return true } func (c *SingerConcurrencyRunner) unmarkRunning() (ok bool) { c.Lock() defer c.Unlock() if !c.isRunning { return false } c.isRunning = false return true } func (c *SingerConcurrencyRunner) Run(f func()) { // Sofort zurückkehren, wenn bereits ausgeführt wird, um übermäßige Speichernutzung zu vermeiden if c.isRunning { return } if !c.markRunning() { // Zurückkehren, wenn das Run-Flag nicht abgerufen werden kann return } // Die eigentliche Logik ausführen go func() { defer func() { if err := recover(); err != nil { // Fehler protokollieren } }() f() c.unmarkRunning() }() }
Zuverlässigkeitstest: Überprüfen, ob mehr als 2 Goroutinen ausgeführt werden
func TestConcurrency(t *testing.T) { runner := NewConcurrencyRunner() for i := 0; i < 100000; i++ { runner.Run(f) } } func f() { // Dies wird niemals die zulässige Anzahl von Goroutinen überschreiten if runtime.NumGoroutine() > 3 { fmt.Println(">3", runtime.NumGoroutine()) } }
2. Festlegen der Anzahl gleichzeitiger Goroutinen
Andere Goroutinen können so eingestellt werden, dass sie mit einem Timeout warten oder auf die Verwendung alter Daten zurückgreifen, anstatt zu warten.
Die Verwendung von Tunny ermöglicht die Steuerung der Anzahl der Goroutinen. Wenn alle Worker
belegt sind, wird die WorkRequest
nicht sofort verarbeitet, sondern in reqChan
in die Warteschlange gestellt, um auf die Verfügbarkeit zu warten.
func (w *workerWrapper) run() { //... for { // HINWEIS: Das Blockieren hier verhindert, dass der Worker herunterfährt. w.worker.BlockUntilReady() select { case w.reqChan <- workRequest{ jobChan: jobChan, retChan: retChan, interruptFunc: w.interrupt, }: select { case payload := <-jobChan: result := w.worker.Process(payload) select { case retChan <- result: case <-w.interruptChan: w.interruptChan = make(chan struct{}) } //... } } //... }
Die Implementierung hier verwendet residente Goroutinen. Wenn die Size
geändert wird, werden neue Worker
erstellt, um die Aufgaben zu bearbeiten. Ein anderer Implementierungsansatz besteht darin, ein chan
zu verwenden, um zu steuern, ob eine Goroutine gestartet werden kann. Wenn der Puffer voll ist, wird keine neue Goroutine gestartet, um die Aufgabe zu bearbeiten.
type ProcessFunc func(ctx context.Context, param interface{}) type MultiConcurrency struct { ch chan struct{} f ProcessFunc } func NewMultiConcurrency(size int, f ProcessFunc) *MultiConcurrency { return &MultiConcurrency{ ch: make(chan struct{}, size), f: f, } } func (m *MultiConcurrency) Run(ctx context.Context, param interface{}) { // Nicht eintreten, wenn der Puffer voll ist m.ch <- struct{}{} go func() { defer func() { // Einen Slot im Puffer freigeben <-m.ch if err := recover(); err != nil { fmt.Println(err) } }() m.f(ctx, param) }() }
Testen, um sicherzustellen, dass die Anzahl der Goroutinen nicht 13 überschreitet:
func mockFunc(ctx context.Context, param interface{}) { fmt.Println(param) } func TestNewMultiConcurrency_Run(t *testing.T) { concurrency := NewMultiConcurrency(10, mockFunc) for i := 0; i < 1000; i++ { concurrency.Run(context.Background(), i) if runtime.NumGoroutine() > 13 { fmt.Println("goroutine", runtime.NumGoroutine()) } } }
Mit diesem Ansatz muss das System nicht viele ausgeführte Goroutinen im Speicher halten. Aber selbst wenn 100 Goroutinen resident sind, beträgt die Speichernutzung nur 2 KB × 100 = 200 KB, was im Grunde vernachlässigbar ist.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Serverlose Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Mio. Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollständig automatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ