Informationen zu gepufferten Kanälen

Abgeschlossen

Wie Sie bereits erfahren haben, werden Kanäle standardmäßig nicht gepuffert. Das bedeutet, dass sie einen Sendevorgang nur dann akzeptieren, wenn ein Empfangsvorgang stattfindet. Andernfalls wird das Programm blockiert und wartet immer weiter.

Es gibt Situationen, in denen diese Art der Synchronisierung zwischen Goroutinen erforderlich ist. Es kann aber auch vorkommen, dass Sie einfach Nebenläufigkeit implementieren müssen und dabei nicht einzuschränken brauchen, wie Goroutinen miteinander kommunizieren.

Gepufferte Kanäle senden und empfangen Daten ohne Blockieren des Programms, da sich ein gepufferter Kanal wie eine Warteschlange verhält. Sie können die Größe dieser Warteschlange beim Erstellen des Kanals wie folgt einschränken:

ch := make(chan string, 10)

Jedes Mal, wenn Sie etwas an den Kanal senden, wird das Element der Warteschlange hinzugefügt. Anschließend wird das Element durch einen Empfangsvorgang aus der Warteschlange entfernt. Wenn der Kanal voll ist, wartet jeder Sendevorgang einfach, bis Platz für die Daten vorhanden ist. Wenn der Kanal hingegen leer ist und ein Lesevorgang stattfindet, wird dieser blockiert, bis ein Element zum Lesen vorhanden ist.

Hier sehen Sie ein einfaches Beispiel zum Verstehen gepufferter Kanäle:

package main

import (
    "fmt"
)

func send(ch chan string, message string) {
    ch <- message
}

func main() {
    size := 4
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    send(ch, "three")
    send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < size; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

Wenn Sie das Programm ausführen, wird die folgende Ausgabe angezeigt:

All data sent to the channel ...
one
two
three
four
Done!

Sie sagen vielleicht, dass wir hier gar nichts anders gemacht haben, und da haben Sie auch recht. Sehen wir uns aber mal an, was geschieht, wenn Sie die size-Variable wie folgt in eine niedrigere Zahl ändern (Sie können es auch mit einer höheren Zahl versuchen):

size := 2

Wenn Sie das Programm erneut ausführen, wird der folgende Fehler angezeigt:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.send(...)
        /Users/developer/go/src/concurrency/main.go:8
main.main()
        /Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2

Der Grund dafür ist, dass die Aufrufe der send-Funktion sequenziell erfolgen. Sie erstellen keine neue Goroutine. Daher muss nichts in die Warteschlange gestellt werden.

Kanäle sind eng mit Goroutinen verbunden. Ohne eine weitere Goroutine, die Daten aus dem Kanal empfängt, kann das gesamte Programm für immer blockieren. Wie Sie gesehen haben, passiert das auch.

Jetzt machen wir etwas Interessantes! Wir erstellen eine Go-Routine für die letzten beiden Aufrufe (die ersten beiden Aufrufe passen gut in den Puffer) und führen eine for-Schleife vier Mal aus. Der Code lautet wie folgt:

func main() {
    size := 2
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    go send(ch, "three")
    go send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < 4; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

Wenn Sie das Programm ausführen, funktioniert es erwartungsgemäß. Es wird empfohlen, dass Sie bei Verwendung von Kanälen immer Goroutinen verwenden.

Testen wir nun den Fall, in dem Sie einen gepufferten Kanal mit mehr Elementen erstellen, als Sie benötigen. Dazu verwenden wir das Beispiel, das wir zuvor zum Überprüfen von APIs verwendet haben, und erstellen einen gepufferten Kanal mit einer Größe von 10:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string, 10)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Wenn Sie das Programm ausführen, erhalten Sie dieselbe Ausgabe wie zuvor. Sie können versuchsweise die Größe des Kanals in eine niedrigere oder höhere Zahl ändern, und das Programm wird trotzdem funktionieren.

Nicht gepufferte und gepufferte Kanäle

An dieser Stelle fragen Sie sich vielleicht, wann der eine oder der andere Typ zu verwenden ist. Das hängt davon ab, wie die Kommunikation zwischen den Goroutinen erfolgen soll. Nicht gepufferte Kanäle kommunizieren synchron. Damit wird sichergestellt, dass das Programm bei jedem Senden von Daten blockiert wird, bis diese aus dem Kanal gelesen werden.

Bei gepufferten Kanälen hingegen sind die Sende- und Empfangsvorgänge nicht aneinander gekoppelt. Ein Programm wird dadurch nicht blockiert, doch Sie müssen vorsichtig sein, da möglicherweise ein Deadlock verursacht wird (wie bereits gesehen). Bei Verwendung nicht gepufferter Kanäle können Sie steuern, wie viele Goroutinen gleichzeitig ausgeführt werden können. Sie möchten möglicherweise Aufrufe für eine API durchführen und die Anzahl der pro Sekunde ausgeführten Aufrufe steuern. Andernfalls kann eine Blockierung auftreten.

Kanalrichtungen

Kanäle in Go verfügen über ein weiteres interessantes Feature. Wenn Sie Kanäle als Parameter für eine Funktion verwenden, können Sie angeben, ob ein Kanal Daten senden oder empfangen soll. Wenn das Programm umfangreicher wird, sind möglicherweise zu viele Funktionen vorhanden, und es empfiehlt sich, den Zweck jedes Kanals zu dokumentieren, um diese richtig zu verwenden. Vielleicht schreiben Sie auch eine Bibliothek und möchten einen Kanal schreibgeschützt verfügbar machen, um Datenkonsistenz zu gewährleisten.

Sie definieren die Richtung des Kanals auf ähnliche Weise wie beim Lesen oder Empfangen von Daten. Dies erfolgt jedoch beim Deklarieren des Kanals in einem Funktionsparameter. Die Syntax zum Definieren des Kanaltyps als Parameter in einer Funktion sieht wie folgt aus:

chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data

Wenn Sie Daten über einen Kanal senden, der nur empfangen soll, wird beim Kompilieren des Programms ein Fehler angezeigt.

Wir verwenden das folgende Programm als Beispiel für zwei Funktionen, von denen eine Daten liest und eine andere Daten sendet:

package main

import "fmt"

func send(ch chan<- string, message string) {
    fmt.Printf("Sending: %#v\n", message)
    ch <- message
}

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
}

func main() {
    ch := make(chan string, 1)
    send(ch, "Hello World!")
    read(ch)
}

Wenn Sie das Programm ausführen, wird die folgende Ausgabe angezeigt:

Sending: "Hello World!"
Receiving: "Hello World!"

Im Programm wird der Zweck der einzelnen Kanäle in jeder Funktion verdeutlicht. Wenn Sie versuchen, einen Kanal zum Senden von Daten zu verwenden, obwohl dieser nur zum Empfangen von Daten dient, wird ein Kompilierungsfehler angezeigt. Versuchen Sie z. B. Folgendes:

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
    ch <- "Bye!"
}

Wenn Sie das Programm ausführen, wird der folgende Fehler angezeigt:

# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)

Es ist besser, einen Kompilierungsfehler zu erhalten, als einen Kanal falsch zu verwenden.

Multiplexing

Zum Schluss sehen wir uns noch an, wie Sie mithilfe des Schlüsselworts select mit mehr als einem Kanal gleichzeitig interagieren können. Manchmal möchten Sie beim Arbeiten mit mehreren Kanälen warten, bis ein Ereignis eintritt. Beispielsweise können Sie eine Logik zum Abbrechen eines Vorgangs einschließen, wenn eine Anomalie bei den vom Programm verarbeiteten Daten vorliegt.

Eine select-Anweisung funktioniert wie eine switch-Anweisung, jedoch für Kanäle. Die Ausführung des Programms wird blockiert, bis ein zu verarbeitendes Ereignis empfangen wird. Wenn mehr als ein Ereignis eintrifft, wird eines nach dem Zufallsprinzip ausgewählt.

Ein wesentlicher Aspekt der select-Anweisung ist, dass die Ausführung nach der Verarbeitung eines Ereignisses beendet wird. Wenn Sie warten möchten, bis weitere Ereignisse auftreten, müssen Sie ggf. eine Schleife verwenden.

Wir verwenden das folgende Programm, um select in Aktion zu sehen:

package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Done processing!"
}

func replicate(ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- "Done replicating!"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go process(ch1)
    go replicate(ch2)

    for i := 0; i < 2; i++ {
        select {
        case process := <-ch1:
            fmt.Println(process)
        case replicate := <-ch2:
            fmt.Println(replicate)
        }
    }
}

Wenn Sie das Programm ausführen, wird die folgende Ausgabe angezeigt:

Done replicating!
Done processing!

Beachten Sie, dass die replicate-Funktion zuerst abgeschlossen wurde, weshalb deren Ausgabe zuerst im Terminal angezeigt wird. Die main-Funktion weist eine Schleife auf, da die select-Anweisung beendet wird, sobald sie ein Ereignis empfängt, doch wird noch weiter auf den Abschluss der process-Funktion gewartet.