Go Concurrency Synchronisation: Alles auf einmal lernen
Wenhao Wang
Dev Intern · Leapcell

Analyse der Methoden zur Steuerung von Goroutinen-Concurrency in Go
Im Go-Sprachprogrammiersystem bietet Goroutine als leichtgewichtiger Thread eine leistungsstarke Unterstützung für die effiziente Implementierung von Concurrent-Operationen aufgrund ihrer signifikanten Vorteile von geringem Ressourcenverbrauch und niedrigen Umschaltkosten. Die effektive Steuerung dieser gleichzeitig laufenden Goroutinen ist jedoch zu einem wichtigen Problem geworden, mit dem sich Entwickler auseinandersetzen müssen.
Wenn es um Concurrency-Kontrolle geht, ist der Locking-Mechanismus oft das erste Mittel, das in Betracht gezogen wird. In der Go-Sprache werden auch relevante Locking-Mechanismen bereitgestellt, einschließlich des Mutual Exclusion Lock sync.Mutex
und des Read-Write Lock sync.RWMutex
, und zusätzlich verfügt sie auch über atomare Operationen sync/atomic
. Es sollte jedoch klar sein, dass sich diese Operationen hauptsächlich auf die Datensicherheit während der Concurrency konzentrieren und nicht direkt auf die Goroutine selbst abzielen.
Dieser Artikel konzentriert sich auf die Einführung gängiger Methoden zur Steuerung des Concurrent-Verhaltens von Goroutinen. In der Go-Sprache gibt es drei am häufigsten verwendete Methoden, nämlich sync.WaitGroup
, channel
und Context
.
I. sync.WaitGroup
sync.WaitGroup
ist ein sehr praktisches Synchronisationsprimitiv in der Go-Sprache, dessen Hauptfunktion darin besteht, Entwickler beim Warten auf die vollständige Ausführung einer Gruppe von Goroutinen zu unterstützen. Normalerweise wird sync.WaitGroup
in den folgenden Szenarien verwendet:
- Wenn sichergestellt werden muss, dass eine Gruppe von Goroutinen alle ausgeführt wurden, bevor das Programm in der Hauptfunktion beendet wird.
- In einem Szenario, in dem mehrere Goroutinen innerhalb einer Funktion gestartet werden und das Ergebnis erst zurückgegeben werden soll, wenn diese Goroutinen alle abgeschlossen sind.
- Wenn eine Funktion mehrere Goroutinen startet und eine bestimmte Operation ausgeführt werden muss, nachdem sie alle abgeschlossen wurden.
- In einer Situation, in der eine Funktion mehrere Goroutinen startet und eine bestimmte Ressource geschlossen werden muss, nachdem sie alle abgeschlossen wurden.
- In einem Fall, in dem eine Funktion mehrere Goroutinen startet und die Schleife erst verlassen werden kann, wenn sie alle abgeschlossen wurden.
Bei der Verwendung von sync.WaitGroup
sind die spezifischen Schritte wie folgt: Erstellen Sie zuerst ein sync.WaitGroup
-Objekt; Verwenden Sie dann die Add
-Methode dieses Objekts, um die Anzahl der Goroutinen anzugeben, auf die gewartet werden soll. Starten Sie anschließend mithilfe des Schlüsselworts go
mehrere Goroutinen und rufen Sie in jeder Goroutine die Done
-Methode des sync.WaitGroup
-Objekts auf, um anzugeben, dass die Goroutine die Ausführung abgeschlossen hat; Rufen Sie schließlich die Wait
-Methode des sync.WaitGroup
-Objekts auf, um auf den Abschluss aller Goroutinen zu warten.
Das Folgende ist ein einfaches Beispiel. Dieses Beispiel startet 3 Goroutinen, die 0 Sekunden, 1 Sekunde bzw. 2 Sekunden schlafen, und die Hauptfunktion wird beendet, nachdem diese 3 Goroutinen beendet wurden:
package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(i int) { defer wg.Done() fmt.Printf("sub goroutine sleep: %ds\n", i) time.Sleep(time.Duration(i) * time.Second) }(i) } wg.Wait() fmt.Println("main func done") }
II. channel
In der Go-Sprache ist channel
ein leistungsstarkes Tool, mit dem Entwickler die Concurrency von Goroutinen besser steuern können. Im Folgenden werden verschiedene gängige Methoden zur Verwendung von channel
zur Steuerung der Concurrency von Goroutinen beschrieben:
(I) Verwenden eines ungepufferten Kanals zur Synchronisation
Ein ungepufferter channel
kann verwendet werden, um das Producer-Consumer-Muster zu implementieren. In diesem Muster ist eine Goroutine für die Erzeugung von Daten verantwortlich und eine andere Goroutine für die Verwendung von Daten. Wenn die Producer-Goroutine Daten an den channel
sendet, wechselt die Consumer-Goroutine in einen blockierten Zustand und wartet auf die Ankunft der Daten. Auf diese Weise kann die Datensynchronisation zwischen dem Producer und dem Consumer sichergestellt werden.
Das Folgende ist ein einfacher Beispielcode:
package main import ( "fmt" "sync" "time" ) func producer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 10; i++ { ch <- i fmt.Println("produced", i) time.Sleep(100 * time.Millisecond) } close(ch) } func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := range ch { fmt.Println("consumed", i) time.Sleep(150 * time.Millisecond) } } func main() { var wg sync.WaitGroup ch := make(chan int) wg.Add(2) go producer(ch, &wg) go consumer(ch, &wg) wg.Wait() }
In diesem Beispiel wird ein ungepufferter channel
erstellt, um Daten zwischen der Producer-Goroutine und der Consumer-Goroutine zu übertragen. Die Producer-Goroutine sendet Daten an den channel
und die Consumer-Goroutine empfängt Daten vom channel
. In der Producer-Goroutine wird die Funktion time.Sleep
verwendet, um die zum Erzeugen von Daten benötigte Zeit zu simulieren; In der Consumer-Goroutine wird auch die Funktion time.Sleep
verwendet, um die zum Verbrauchen von Daten benötigte Zeit zu simulieren. Schließlich wird sync.WaitGroup
verwendet, um auf den Abschluss aller Goroutinen zu warten.
(II) Verwenden eines gepufferten Kanals zur Ratenbegrenzung
Ein gepufferter channel
kann verwendet werden, um die Anzahl der Concurrent-Goroutinen zu begrenzen. Der spezifische Ansatz besteht darin, die Kapazität des channel
auf die gewünschte maximale Anzahl von Concurrent-Goroutinen festzulegen. Bevor jede Goroutine gestartet wird, senden Sie einen Wert an den channel
; Wenn die Goroutine die Ausführung beendet, empfangen Sie einen Wert aus dem channel
. Auf diese Weise kann sichergestellt werden, dass die Anzahl der gleichzeitig ausgeführten Goroutinen die angegebene maximale Concurrent-Anzahl nicht überschreitet.
Das Folgende ist ein einfacher Beispielcode:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup maxConcurrency := 3 semaphore := make(chan struct{}, maxConcurrency) for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() semaphore <- struct{}{} fmt.Println("goroutine", i, "started") // do some work fmt.Println("goroutine", i, "finished") <-semaphore }() } wg.Wait() }
In diesem Beispiel wird ein gepufferter channel
erstellt, dessen Puffergröße 3 beträgt. Dann werden 10 Goroutinen gestartet. In jeder Goroutine wird eine leere Struktur an den channel
gesendet, um anzuzeigen, dass die Goroutine mit der Ausführung begonnen hat; Wenn die Goroutine abgeschlossen ist, wird eine leere Struktur aus dem channel
empfangen, um anzuzeigen, dass die Goroutine die Ausführung abgeschlossen hat. Auf diese Weise kann sichergestellt werden, dass die Anzahl der gleichzeitig ausgeführten Goroutinen 3 nicht überschreitet.
III. Context
In der Go-Sprache ist Context
ein wichtiges Mittel zur Steuerung der Concurrency von Goroutinen. Im Folgenden werden verschiedene gängige Methoden zur Verwendung von Context
zur Steuerung der Concurrency von Goroutinen beschrieben:
(I) Timeout-Kontrolle
In einigen Fällen ist es zur Vermeidung von Problemen wie langfristiger Blockierung oder Deadlock des Programms erforderlich, die Ausführungszeit von Goroutinen zu begrenzen. Context
kann Entwicklern helfen, die Ausführungszeit von Goroutinen besser zu steuern. Die spezifische Operation besteht darin, einen Context
mit einem Timeout-Zeitraum zu erstellen und ihn an die Goroutine zu übergeben. Wenn die Goroutine die Ausführung nicht innerhalb des Timeout-Zeitraums abschließt, kann die Done
-Methode des Context
verwendet werden, um die Ausführung der Goroutine abzubrechen.
Das Folgende ist ein einfacher Beispielcode:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go func() { for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(3 * time.Second) }
In diesem Beispiel wird ein Context
mit einem Timeout-Zeitraum erstellt und an die Goroutine übergeben. Innerhalb der Goroutine wird die select
-Anweisung verwendet, um die Done
-Methode des Context
zu überwachen. Wenn für Context
ein Timeout auftritt, wird die Ausführung der Goroutine abgebrochen.
(II) Abbruchoperation
Während des laufenden Prozesses des Programms ist es manchmal notwendig, die Ausführung bestimmter Goroutinen abzubrechen. Context
kann Entwicklern helfen, die Abbruchoperation von Goroutinen besser zu steuern. Der spezifische Ansatz besteht darin, einen Context
mit einer Abbruchfunktion zu erstellen und ihn an die Goroutine zu übergeben. Wenn es notwendig ist, die Ausführung der Goroutine abzubrechen, kann die Cancel
-Methode des Context
aufgerufen werden.
Das Folgende ist ein einfacher Beispielcode:
package main import ( "context" "fmt" "sync" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(2 * time.Second) cancel() wg.Wait() }
In diesem Beispiel wird ein Context
mit einer Abbruchfunktion erstellt und an die Goroutine übergeben. Innerhalb der Goroutine wird die select
-Anweisung verwendet, um die Done
-Methode des Context
zu überwachen. Wenn der Context
abgebrochen wird, wird die Ausführung der Goroutine abgebrochen. In der Hauptfunktion wird die Funktion time.Sleep
verwendet, um einen bestimmten Moment während des laufenden Prozesses des Programms zu simulieren, in dem es notwendig ist, die Ausführung der Goroutine abzubrechen, und dann wird die Cancel
-Methode des Context
aufgerufen.
(III) Ressourcenverwaltung
In einigen Szenarien ist es notwendig, die von Goroutinen verwendeten Ressourcen zu verwalten, um Probleme wie Ressourcenlecks oder Race-Bedingungen zu vermeiden. Context
kann Entwicklern helfen, die von Goroutinen verwendeten Ressourcen besser zu verwalten. Die spezifische Operation besteht darin, die Ressourcen dem Context
zuzuordnen und den Context
an die Goroutine zu übergeben. Wenn die Goroutine die Ausführung beendet, kann der Context
verwendet werden, um die Ressourcen freizugeben oder andere Ressourcenverwaltungsoperationen durchzuführen.
Das Folgende ist ein einfacher Beispielcode:
package main import ( "context" "fmt" "sync" "time" ) func worker(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go worker(ctx, &wg) time.Sleep(2 * time.Second) cancel() wg.Wait() }
In diesem Beispiel wird ein Context
mit einer Abbruchfunktion erstellt und an die Goroutine übergeben. Innerhalb der Goroutine wird die select
-Anweisung verwendet, um die Done
-Methode des Context
zu überwachen. Wenn der Context
abgebrochen wird, wird die Ausführung der Goroutine abgebrochen. In der Hauptfunktion wird die Funktion time.Sleep
verwendet, um einen bestimmten Moment während des laufenden Prozesses des Programms zu simulieren, in dem es notwendig ist, die Ausführung der Goroutine abzubrechen, und dann wird die Cancel
-Methode des Context
aufgerufen.
Leapcell: The Next-Gen Serverless-Plattform für Golang-App-Hosting
Abschließend möchte ich eine Plattform empfehlen, die sich am besten für die Bereitstellung von Go-Diensten eignet: Leapcell
1. Multi-Language-Support
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Mio. Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und Protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Autoskalierung zur einfachen Bewältigung hoher Concurrency.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ