Meer informatie over gebufferde kanalen

Voltooid

Zoals u hebt geleerd, worden kanalen standaard niet-gebufferd. Dat betekent dat ze alleen een verzendbewerking accepteren als er een ontvangstbewerking is. Anders wordt het programma voor altijd geblokkeerd.

Er zijn momenten waarop u dat type synchronisatie tussen goroutines nodig hebt. Het kan echter voorkomen dat u gewoon gelijktijdigheid moet implementeren en u niet hoeft te beperken hoe goroutines met elkaar communiceren.

Gebufferde kanalen verzenden en ontvangen gegevens zonder het programma te blokkeren, omdat een gebufferd kanaal zich gedraagt als een wachtrij. U kunt de grootte van deze wachtrij beperken wanneer u het kanaal maakt, zoals deze:

ch := make(chan string, 10)

Telkens wanneer u iets naar het kanaal verzendt, wordt het element toegevoegd aan de wachtrij. Vervolgens verwijdert een ontvangstbewerking het element uit de wachtrij. Wanneer het kanaal vol is, wacht elke verzendbewerking totdat er ruimte is om de gegevens op te bewaren. Als het kanaal leeg is en er een leesbewerking is, wordt het geblokkeerd totdat er iets te lezen is.

Hier volgt een eenvoudig voorbeeld om inzicht te krijgen in gebufferde kanalen:

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!")
}

Wanneer u het programma uitvoert, ziet u de volgende uitvoer:

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

Je zou kunnen zeggen dat we hier niets anders hebben gedaan, en je zou gelijk hebben. Maar laten we eens kijken wanneer u de size variabele wijzigt in een lager getal (u kunt zelfs proberen met een hoger getal), zoals hieronder:

size := 2

Wanneer u het programma opnieuw uitvoert, krijgt u de volgende fout:

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

De reden hiervoor is dat de aanroepen naar de send functie opeenvolgend zijn. U maakt geen nieuwe goroutine. Daarom is er niets om in de wachtrij te plaatsen.

Kanalen zijn diep verbonden met goroutines. Zonder een andere goroutine die gegevens van het kanaal ontvangt, kan het hele programma voor altijd in een blok terechtkomen. Zoals je hebt gezien, gebeurt het wel.

Laten we nu iets interessants maken! We maken een goroutine voor de laatste twee aanroepen (de eerste twee aanroepen passen goed in de buffer) en maken vier keer een lusuitvoering. Hier volgt de code:

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!")
}

Wanneer u het programma uitvoert, werkt het zoals verwacht. We raden u aan om bij het gebruik van kanalen altijd goroutines te gebruiken.

We gaan de case testen waarin u een gebufferd kanaal maakt met meer elementen dan u nodig hebt. We gebruiken het voorbeeld dat we eerder hebben gebruikt om API's te controleren en een gebufferd kanaal met een grootte van 10 te maken:

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)
}

Wanneer u het programma uitvoert, krijgt u dezelfde uitvoer als voorheen. U kunt spelen door de grootte van het kanaal te wijzigen met lagere of hogere getallen en het programma werkt nog steeds.

Niet-gebufferde versus gebufferde kanalen

Op dit moment vraagt u zich misschien af wanneer u het ene type of het andere wilt gebruiken. Het hangt allemaal af van hoe u wilt dat de communicatie tussen goroutines stroomt. Niet-gebufferde kanalen communiceren synchroon. Ze garanderen dat telkens wanneer u gegevens verzendt, het programma wordt geblokkeerd totdat iemand van het kanaal leest.

Omgekeerd ontkoppelen gebufferde kanalen de verzend- en ontvangstbewerkingen. Ze blokkeren een programma niet, maar je moet voorzichtig zijn omdat je uiteindelijk een impasse veroorzaakt (zoals je eerder hebt gezien). Wanneer u niet-gebufferde kanalen gebruikt, kunt u bepalen hoeveel goroutines gelijktijdig kunnen worden uitgevoerd. U kunt bijvoorbeeld aanroepen naar een API maken en u wilt bepalen hoeveel aanroepen u elke seconde uitvoert. Anders wordt u mogelijk geblokkeerd.

Kanaalbeschrijving

Kanalen in Go hebben een andere interessante functie. Wanneer u kanalen als parameters voor een functie gebruikt, kunt u opgeven of een kanaal bedoeld is om gegevens te verzenden of te ontvangen . Naarmate uw programma groeit, hebt u mogelijk te veel functies en is het een goed idee om de intentie van elk kanaal te documenteren om ze correct te gebruiken. Of misschien schrijft u een bibliotheek en wilt u een kanaal beschikbaar maken als alleen-lezen om gegevensconsistentie te behouden.

Als u de richting van het kanaal wilt definiëren, doet u dit op een vergelijkbare manier als wanneer u gegevens leest of ontvangt. Maar u doet dit wanneer u het kanaal declareren in een functieparameter. De syntaxis voor het definiëren van het type kanaal als parameter in een functie is:

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

Wanneer u gegevens verzendt via een kanaal dat alleen-ontvangen is, krijgt u een foutmelding bij het compileren van het programma.

Laten we het volgende programma gebruiken als voorbeeld van twee functies, een functie die gegevens leest en een andere waarmee gegevens worden verzonden:

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)
}

Wanneer u het programma uitvoert, ziet u de volgende uitvoer:

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

Het programma verduidelijkt de intentie van elk kanaal in elke functie. Als u een kanaal probeert te gebruiken om gegevens te verzenden in een kanaal waarvan het doel alleen is om gegevens te ontvangen, krijgt u een compilatiefout. Probeer bijvoorbeeld iets als volgt te doen:

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

Wanneer u het programma uitvoert, ziet u de volgende fout:

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

Het is beter om een compilatiefout te hebben dan een kanaal te misbruiken.

Multiplexen

Laten we ten slotte zien hoe u met meer dan één kanaal tegelijk kunt werken met behulp van het select trefwoord. Soms wilt u wachten tot er een gebeurtenis plaatsvindt wanneer u met meerdere kanalen werkt. U kunt bijvoorbeeld logica opnemen om een bewerking te annuleren wanneer er een anomalie is in de gegevens die door uw programma worden verwerkt.

Een select instructie werkt als een switch instructie, maar voor kanalen. De uitvoering van het programma wordt geblokkeerd totdat er een gebeurtenis wordt verwerkt. Als er meer dan één gebeurtenis wordt weergegeven, wordt er een willekeurig gekozen.

Een essentieel aspect van de select instructie is dat de uitvoering is voltooid nadat een gebeurtenis is verwerkt. Als u wilt wachten tot er meer gebeurtenissen plaatsvinden, moet u mogelijk een lus gebruiken.

Laten we het volgende programma gebruiken om in actie te zien select :

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)
        }
    }
}

Wanneer u het programma uitvoert, ziet u de volgende uitvoer:

Done replicating!
Done processing!

U ziet dat de replicate functie eerst is voltooid. Daarom ziet u eerst de uitvoer in de terminal. De hoofdfunctie heeft een lus omdat de select instructie eindigt zodra deze een gebeurtenis ontvangt, maar we wachten nog steeds tot de process functie is voltooid.