Effiziente Go Concurrency mit select
Min-jun Kim
Dev Intern · Leapcell

Vorwort
In der Go-Programmiersprache sind Goroutinen und Kanäle wesentliche Konzepte der nebenläufigen Programmierung. Sie helfen bei der Lösung verschiedener Probleme im Zusammenhang mit Nebenläufigkeit. Dieser Artikel konzentriert sich auf select
, das als Brücke zur Koordinierung mehrerer Kanäle dient.
Einführung in select
Was ist select
select
ist eine Kontrollstruktur in Go, die verwendet wird, um eine ausführbare Operation unter mehreren Kommunikationsoperationen auszuwählen. Sie koordiniert Lese- und Schreiboperationen auf mehreren Kanälen und ermöglicht so eine nicht-blockierende Datenübertragung, Synchronisation und Steuerung über mehrere Kanäle.
Warum brauchen wir select
Die select
-Anweisung in Go bietet einen Mechanismus für das Multiplexen von Kanälen. Sie ermöglicht es uns, auf Nachrichten auf mehreren Kanälen zu warten und diese zu verarbeiten. Im Vergleich zur einfachen Verwendung einer for
-Schleife zur Iteration über Kanäle ist select
eine effizientere Möglichkeit, mehrere Kanäle zu verwalten.
Hier sind einige gängige Szenarien für die Verwendung von select
:
-
Warten auf Nachrichten von mehreren Kanälen (Multiplexing) Wenn wir auf Nachrichten von mehreren Kanälen warten müssen, macht es
select
bequem, auf den Empfang von Daten von einem dieser Kanäle zu warten, wodurch die Notwendigkeit entfällt, mehrere Goroutinen für die Synchronisation und das Warten zu verwenden. -
Timeout beim Warten auf Kanalnachrichten Wenn wir innerhalb eines bestimmten Zeitraums auf eine Nachricht von einem Kanal warten müssen, kann
select
mit demtime
-Paket kombiniert werden, um ein zeitgesteuertes Warten zu implementieren. -
Nicht-blockierende Lese-/Schreibvorgänge auf Kanälen Das Lesen von oder Schreiben in einen Kanal blockiert, wenn der Kanal keine Daten bzw. keinen Speicherplatz hat. Die Verwendung von
select
mit einemdefault
-Zweig ermöglicht nicht-blockierende Operationen und vermeidet Deadlocks oder Endlosschleifen.
Daher ist der Hauptzweck von select
, einen effizienten und einfach zu bedienenden Mechanismus für die Handhabung mehrerer Kanäle bereitzustellen, die Goroutinen-Synchronisation und das Warten zu vereinfachen und Programme lesbarer, effizienter und zuverlässiger zu machen.
Grundlagen von select
Syntax
select { case <- channel1: // channel1 ist bereit case data := <- channel2: // channel2 ist bereit, und Daten können gelesen werden case channel3 <- data: // channel3 ist bereit, und Daten können hineingeschrieben werden default: // kein Kanal ist bereit }
Hier bedeutet <- channel1
das Lesen von channel1
, und data := <- channel2
bedeutet das Empfangen von Daten in data
. channel3 <- data
bedeutet das Schreiben von data
in channel3
.
Die Syntax von select
ähnelt switch
, wird aber ausschließlich für Kanaloperationen verwendet. In einer select
-Anweisung können wir mehrere case
-Blöcke definieren, die jeweils eine Kanaloperation zum Lesen oder Schreiben von Daten darstellen. Wenn mehrere Fälle gleichzeitig bereit sind, wird einer von ihnen zufällig ausgewählt. Wenn keiner bereit ist, wird der default
-Zweig (falls vorhanden) ausgeführt; andernfalls blockiert der select
, bis mindestens ein Fall bereit ist.
Grundlegende Verwendung
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { time.Sleep(1 * time.Second) ch1 <- 1 }() go func() { time.Sleep(2 * time.Second) ch2 <- 2 }() for i := 0; i < 2; i++ { select { case data, ok := <-ch1: if ok { fmt.Println("Empfangen von ch1:", data) } else { fmt.Println("Kanal geschlossen") } case data, ok := <-ch2: if ok { fmt.Println("Empfangen von ch2:", data) } else { fmt.Println("Kanal geschlossen") } } } select { case data, ok := <-ch1: if ok { fmt.Println("Empfangen von ch1:", data) } else { fmt.Println("Kanal geschlossen") } case data, ok := <-ch2: if ok { fmt.Println("Empfangen von ch2:", data) } else { fmt.Println("Kanal geschlossen") } default: fmt.Println("Keine Daten empfangen, Standardzweig ausgeführt") } }
Ausführungsergebnis
Empfangen von ch1: 1
Empfangen von ch2: 2
Keine Daten empfangen, Standardzweig ausgeführt
Im obigen Beispiel werden zwei Kanäle ch1
und ch2
erstellt. Separate Goroutinen schreiben nach unterschiedlichen Verzögerungen in diese Kanäle. Die Haupt-Goroutine überwacht beide Kanäle mithilfe einer select
-Anweisung. Wenn Daten auf einem Kanal eintreffen, werden diese ausgegeben. Da ch1
vor ch2
Daten empfängt, wird zuerst die Meldung "Empfangen von ch1: 1"
und dann "Empfangen von ch2: 2"
ausgegeben.
Um den default
-Zweig zu demonstrieren, enthält das Programm einen zweiten select
-Block. An diesem Punkt sind sowohl ch1
als auch ch2
leer, sodass der default
-Zweig ausgeführt wird und "Keine Daten empfangen, Standardzweig ausgeführt"
ausgibt.
Szenarien, die select
und Kanäle kombinieren
Implementierung der Timeout-Steuerung
package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { time.Sleep(3 * time.Second) ch <- 1 }() select { case data, ok := <-ch: if ok { fmt.Println("Daten empfangen:", data) } else { fmt.Println("Kanal geschlossen") } case <-time.After(2 * time.Second): fmt.Println("Zeitüberschreitung!") } }
Ausführungsergebnis: Zeitüberschreitung!
In diesem Beispiel sendet das Programm nach 3 Sekunden Daten in den Kanal ch
. Der select
-Block setzt jedoch ein Timeout von 2 Sekunden. Wenn innerhalb dieser Zeit keine Daten empfangen werden, wird der Timeout-Fall ausgelöst.
Implementierung der Multi-Task-Concurrency-Steuerung
package main import ( "fmt" ) func main() { ch := make(chan int) for i := 0; i < 10; i++ { go func(id int) { ch <- id }(i) } for i := 0; i < 10; i++ { select { case data, ok := <-ch: if ok { fmt.Println("Aufgabe abgeschlossen:", data) } else { fmt.Println("Kanal geschlossen") } } } }
Ausführungsergebnis (Reihenfolge kann bei jeder Ausführung variieren):
Aufgabe abgeschlossen: 1
Aufgabe abgeschlossen: 5
Aufgabe abgeschlossen: 2
Aufgabe abgeschlossen: 3
Aufgabe abgeschlossen: 4
Aufgabe abgeschlossen: 0
Aufgabe abgeschlossen: 9
Aufgabe abgeschlossen: 6
Aufgabe abgeschlossen: 7
Aufgabe abgeschlossen: 8
In diesem Beispiel werden 10 Goroutinen gestartet, um Aufgaben gleichzeitig auszuführen. Ein einzelner Kanal wird verwendet, um Benachrichtigungen über den Abschluss von Aufgaben zu empfangen. Die Hauptfunktion überwacht diesen Kanal mithilfe von select
und verarbeitet jede abgeschlossene Aufgabe nach Erhalt.
Überwachen mehrerer Kanäle
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) ch2 := make(chan int) // Starten Sie Goroutine 1, um Daten an ch1 zu senden go func() { for i := 0; i < 5; i++ { ch1 <- i time.Sleep(time.Second) } }() // Starten Sie Goroutine 2, um Daten an ch2 zu senden go func() { for i := 5; i < 10; i++ { ch2 <- i time.Sleep(time.Second) } }() // Die Haupt-Goroutine empfängt und gibt Daten von ch1 und ch2 aus for i := 0; i < 10; i++ { select { case data := <-ch1: fmt.Println("Empfangen von ch1:", data) case data := <-ch2: fmt.Println("Empfangen von ch2:", data) } } fmt.Println("Fertig.") }
Ausführungsergebnis (Reihenfolge kann bei jeder Ausführung variieren):
Empfangen von ch2: 5
Empfangen von ch1: 0
Empfangen von ch1: 1
Empfangen von ch2: 6
Empfangen von ch1: 2
Empfangen von ch2: 7
Empfangen von ch1: 3
Empfangen von ch2: 8
Empfangen von ch1: 4
Empfangen von ch2: 9
Fertig.
In diesem Beispiel ermöglicht select
das Multiplexen von Daten aus mehreren Kanälen. Es ermöglicht dem Programm, ch1
und ch2
gleichzeitig zu überwachen, ohne dass separate Goroutinen für die Synchronisation erforderlich sind.
Verwenden von default
zum Erreichen von nicht-blockierenden Lese- und Schreibvorgängen
import ( "fmt" "time" ) func main() { ch := make(chan int, 1) go func() { for i := 1; i <= 5; i++ { ch <- i time.Sleep(1 * time.Second) } close(ch) }() for { select { case val, ok := <-ch: if ok { fmt.Println(val) } else { ch = nil } default: fmt.Println("Kein Wert bereit") time.Sleep(500 * time.Millisecond) } if ch == nil { break } } }
Ausführungsergebnis (Reihenfolge kann bei jeder Ausführung variieren):
Kein Wert bereit
1
Kein Wert bereit
2
Kein Wert bereit
Kein Wert bereit
3
Kein Wert bereit
Kein Wert bereit
4
Kein Wert bereit
Kein Wert bereit
5
Kein Wert bereit
Kein Wert bereit
Dieser Code verwendet den default
-Zweig, um nicht-blockierende Kanal-Lese- und Schreibvorgänge zu implementieren. In der select
-Anweisung wird, wenn ein Kanal zum Lesen oder Schreiben bereit ist, der entsprechende Zweig ausgeführt. Wenn keine Kanäle bereit sind, wird der default
-Zweig ausgeführt, wodurch eine Blockierung vermieden wird.
Hinweise zur Verwendung von select
Hier sind einige wichtige Punkte, die Sie bei der Verwendung von select
beachten sollten:
select
-Anweisungen können nur für Kommunikationsoperationen verwendet werden, z. B. zum Lesen von oder Schreiben in Kanäle; sie können nicht für normale Berechnungen oder Funktionsaufrufe verwendet werden.- Eine
select
-Anweisung blockiert, bis mindestens ein Fall bereit ist. Wenn mehrere Fälle bereit sind, wird einer von ihnen zufällig ausgewählt. - Wenn keine Fälle bereit sind und ein
default
-Zweig vorhanden ist, wird derdefault
-Zweig sofort ausgeführt. - Stellen Sie bei der Verwendung von Kanälen in einem
select
sicher, dass die Kanäle ordnungsgemäß initialisiert sind. - Wenn ein Kanal geschlossen ist, kann er weiterhin gelesen werden, bis er leer ist. Das Lesen von einem geschlossenen Kanal gibt den Nullwert des Elementtyps und einen booleschen Wert zurück, der den geschlossenen Status des Kanals angibt.
Zusammenfassend lässt sich sagen, dass Sie bei der Verwendung von select
die Bedingungen und die Ausführungsreihenfolge jedes Falls sorgfältig abwägen sollten, um Deadlocks und andere Probleme zu vermeiden.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Go-Projekten.
Leapcell ist die Next-Gen Serverless Platform 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.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Auto-Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand - konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ