Informationen zu Goroutinen

Abgeschlossen

Nebenläufigkeit ist die Zusammenstellung unabhängiger Aktivitäten, wie z. B. die Arbeit eines Webservers, wenn er mehrere Benutzeranforderungen gleichzeitig, jedoch auf autonome Weise verarbeitet. Nebenläufigkeit ist heute in vielen Programmen vorhanden. Webserver sind ein Beispiel dafür, doch wird Nebenläufigkeit auch bei der Verarbeitung großer Datenmengen in Batches benötigt.

Go bietet zwei Stile für das Schreiben gleichzeitiger Programme. Einer ist der traditionelle Stil, den Sie vielleicht bereits in anderen Sprachen mit Threads verwendet haben. In diesem Modul lernen Sie den Stil von Go kennen, bei dem Werte zwischen unabhängigen Aktivitäten, bezeichnet als Go-Routinen (goroutines), zum Kommunizieren von Prozessen übergeben werden.

Wenn Sie sich zum ersten Mal mit Nebenläufigkeit befassen, sollten Sie sich etwas mehr Zeit zum Durcharbeiten der einzelnen Codeabschnitte nehmen, die wir zur Übung schreiben werden.

Go-Ansatz für Nebenläufigkeit

In der Regel besteht das größte Problem beim Schreiben gleichzeitiger Programme darin, Daten zwischen Prozessen gemeinsam zu nutzen. Go verfolgt bei der Kommunikation einen anderen Ansatz als andere Programmiersprachen, denn Go leitet Daten über Kanäle hin und her. Dieser Ansatz bedeutet, dass nur eine Aktivität (Go-Routine) Zugriff auf die Daten hat und sich entwurfsbedingt keine Racebedingung ergibt. Sie werden in diesem Modul mehr über Goroutinen und Kanäle erfahren und dadurch den Go-Ansatz für Nebenläufigkeit besser verstehen.

Der Ansatz von Go lässt sich in folgendem Slogan zusammenfassen: „Nicht kommunizieren durch Teilen von Arbeitsspeicher, sondern Teilen von Arbeitsspeicher durch Kommunizieren“ Wir werden diesen Ansatz in den folgenden Abschnitten behandeln, aber Sie können auch mehr im Go-Blogbeitrag Teilen von Arbeitsspeicher durch Kommunikation erfahren.

Wie bereits erwähnt, enthält Go auch Primitive für Nebenläufigkeit auf niedriger Ebene. In diesem Modul wird jedoch nur der idiomatische Ansatz von Go für Nebenläufigkeit behandelt.

Befassen wir uns zunächst mit den Goroutinen.

Goroutinen

Bei einer Goroutine handelt es sich um eine gleichzeitige Aktivität in einem leichtgewichtigen Thread und nicht um den herkömmlichen in einem Betriebssystem. Angenommen, Sie haben ein Programm, das in die Ausgabe schreibt, und eine weitere Funktion, die etwas berechnet, z. B. zwei Zahlen addiert. Ein gleichzeitiges Programm kann mehrere Goroutinen aufweisen, die beide Funktionen gleichzeitig aufrufen.

Wir können sagen, dass die erste von einem Programm ausgeführte Goroutine die main()-Funktion ist. Wenn Sie eine weitere Go-Routine erstellen möchten, müssen Sie das Schlüsselwort go wie folgt verwenden, bevor Sie die Funktion aufrufen, wie in diesem Beispiel:

func main(){
    login()
    go launch()
}

Sie werden auch feststellen, dass viele Programme gerne anonyme Funktionen zum Erstellen von Go-Routinen verwenden, wie in diesem Code:

func main(){
    login()
    go func() {
        launch()
    }()
}

Um Go-Routinen in Aktion zu sehen, schreiben wir ein nebenläufiges Programm.

Schreiben eines gleichzeitigen Programms

Da wir uns nur auf den gleichzeitigen Teil konzentrieren wollen, verwenden wir ein bereits vorhandenes Programm, mit dem überprüft wird, ob ein API-Endpunkt reagiert oder nicht. Der Code lautet wie folgt:

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",
    }

    for _, api := range apis {
        _, err := http.Get(api)
        if err != nil {
            fmt.Printf("ERROR: %s is down!\n", api)
            continue
        }

        fmt.Printf("SUCCESS: %s is up and running!\n", api)
    }

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

Wenn Sie den vorherigen Code ausführen, wird die folgende Ausgabe angezeigt:

SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!

Hieran ist nichts Außergewöhnliches, aber es geht noch besser. Vielleicht können wir alle Websites gleichzeitig überprüfen? Anstatt fast zwei Sekunden zu benötigen, könnte das Programm in weniger als 500 ms beendet werden.

Beachten Sie, dass der Codeabschnitt, der gleichzeitig ausgeführt werden muss, der Code für den HTTP-Aufruf der Website ist. Anders ausgedrückt: Es muss eine Goroutine für jede vom Programm überprüfte API erstellt werden.

Zum Erstellen einer Goroutine muss das Schlüsselwort go verwendet werden, bevor eine Funktion aufgerufen wird. Es gibt hier aber keine Funktion. Wir können diesen Code umgestalten und eine neue Funktion wie folgt erstellen:

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

    fmt.Printf("SUCCESS: %s is up and running!\n", api)
}

Beachten Sie, dass das Schlüsselwort continue nicht mehr benötigt wird, da keine for-Schleife vorhanden ist. Um den Ausführungsflow der Funktion zu beenden, verwenden wir das Schlüsselwort return. Nun muss der Code in der main()-Funktion wie folgt geändert werden, um eine Goroutine pro API zu erstellen:

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

Führen Sie das Programm erneut aus, und sehen Sie, was geschieht.

Es sieht so aus, als würde das Programm die APIs nicht mehr überprüfen, richtig? Es könnte in etwa die folgende Ausgabe angezeigt werden:

Done! It took 1.506e-05 seconds!

Das ging schnell! Was ist passiert? Sie sehen die abschließende Meldung, dass das Programm abgeschlossen wurde, da Go eine Go-Routine für jede Position innerhalb der Schleife erstellt hat, und es sofort zur nächsten Zeile gesprungen ist.

Auch wenn es nicht so aussieht, als würde die checkAPI-Funktion ausgeführt, ist das dennoch der Fall. Sie hatte einfach keine Zeit, fertig zu werden. Sehen Sie sich an, was geschieht, wenn Sie direkt hinter der Schleife einen Zeitgeber für den Ruhezustand wie folgt einfügen:

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

time.Sleep(3 * time.Second)

Wenn Sie das Programm nun erneut ausführen, wird möglicherweise eine Ausgabe wie die folgende angezeigt:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!

Es scheint zu funktionieren. Naja, nicht ganz. Was ist, wenn Sie der Liste eine neue Website hinzufügen möchten? Möglicherweise reichen drei Sekunden nicht aus. Wie können Sie das wissen? Sie können das nicht. Es muss eine bessere Möglichkeit geben, und genau damit werden wir uns im nächsten Abschnitt befassen, in dem es um Kanäle geht.