Použití kanálů jako komunikačního mechanismu

Dokončeno

Kanál v Go je komunikační mechanismus mezi goroutinami. Mějte na paměti, že přístup Go pro souběžnost je: "Nekomunikujte sdílením paměti; místo toho sdílejte paměť komunikací." Když potřebujete odeslat hodnotu z jednoho goroutine do jiného, použijte kanály. Pojďme se podívat, jak fungují a jak je můžete začít používat k psaní souběžných programů Go.

Syntaxe kanálu

Protože kanál je komunikační mechanismus, který odesílá a přijímá data, má také typ. To znamená, že můžete odesílat data jenom pro druh, který kanál podporuje. Klíčové slovo chan použijete jako datový typ pro kanál, ale musíte také zadat datový typ, který bude procházet kanálem, jako je int typ.

Pokaždé, když deklarujete kanál nebo chcete určit kanál jako parametr ve funkci, musíte použít chan <type>, například chan int. K vytvoření kanálu použijete integrovanou make() funkci:

ch := make(chan int)

Kanál může provádět dvě operace: odesílat data a přijímat data. Chcete-li určit typ operace, kterou kanál obsahuje, musíte použít operátor kanálu <-. Kromě toho odesílání dat a přijímání dat v kanálech blokují operace. Uvidíte, proč.

Pokud chcete říct, že kanál odesílá jenom data, použijte operátor za <- kanálem. Pokud chcete, aby kanál přijímal data, použijte <- operátor před kanálem, jak je znázorněno v těchto příkladech:

ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded

Další operací, kterou můžete použít v kanálu, je zavřít ji. Pokud chcete kanál zavřít, použijte integrovanou close() funkci:

close(ch)

Když kanál zavřete, říkáte, že data se v daném kanálu znovu neodesílají. Pokud se pokusíte odeslat data do uzavřeného kanálu, program se zpanikaří. A pokud se pokusíte přijímat data z uzavřeného kanálu, budete moct číst všechna odeslaná data. Každé následné čtení pak vrátí nulovou hodnotu.

Vraťme se k programu, který jsme vytvořili dříve, a pomocí kanálů odebereme funkci spánku. Nejprve ve funkci vytvoříme řetězcový kanál main , například takto:

ch := make(chan string)

A odebereme přímku time.Sleep(3 * time.Second)spánku .

Teď můžeme použít kanály ke komunikaci mezi goroutinami. Místo tisku výsledku ve checkAPI funkci refaktorujeme kód a pošleme tuto zprávu přes kanál. Pokud chcete použít kanál z této funkce, musíte kanál přidat jako parametr. Funkce checkAPI by měla vypadat takto:

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

Všimněte si, že funkci musíme použít fmt.Sprintf , protože nechceme tisknout žádný text, stačí poslat formátovaný text přes kanál. Všimněte si také, že k odesílání dat používáme <- operátor za proměnnou kanálu.

Teď je potřeba změnit main funkci tak, aby odesílala proměnnou kanálu a přijímala data, aby se vytiskla, například takto:

ch := make(chan string)

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

fmt.Print(<-ch)

Všimněte si, že operátor používáme <- před kanálem, že chceme číst data z kanálu.

Po opětovném spuštění programu se zobrazí výstup podobný tomuto:

ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!

Aspoň funguje bez volání funkce spánku, že? Ale pořád to nedělá to, co chceme. Vidíme výstup jenom z jednoho z goroutin a vytvořili jsme pět. Pojďme se podívat, proč tento program funguje tímto způsobem v další části.

Nepřipojené kanály

Když vytvoříte kanál pomocí make() funkce, vytvoříte kanál bez vyrovnávacího oznámení, což je výchozí chování. Nepřipojené kanály blokují operaci odesílání, dokud nebude někdo připravený přijímat data. Jak jsme řekli dříve, odesílání a příjem blokuje operace. Tato operace blokování je také důvodem, proč program z předchozí části přestal, jakmile přijal první zprávu.

Můžeme začít tím, že fmt.Print(<-ch) program zablokuje, protože čte z kanálu a čeká na doručení některých dat. Jakmile něco obsahuje, pokračuje dalším řádkem a program se dokončí.

Co se stalo se zbytkem goroutin? Pořád běží, ale nikdo už neposlouchá. A protože program skončil brzy, některé goroutiny nemohly odesílat data. Abychom tento bod prokázali, přidáme další fmt.Print(<-ch), například takto:

ch := make(chan string)

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

fmt.Print(<-ch)
fmt.Print(<-ch)

Po opětovném spuštění programu se zobrazí výstup podobný tomuto:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!

Všimněte si, že teď vidíte výstup pro dvě rozhraní API. Pokud budete pokračovat v přidávání dalších fmt.Print(<-ch) řádků, přečtete si všechna data odesílaná do kanálu. Co se ale stane, když se pokusíte přečíst další data a nikdo už neposílá data? Příklad je podobný tomuto:

ch := make(chan string)

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

fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)

fmt.Print(<-ch)

Po opětovném spuštění programu se zobrazí výstup podobný tomuto:

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://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!

Funguje to, ale program se nedokončí. Poslední řádek tisku ho blokuje, protože očekává příjem dat. Program budete muset zavřít příkazem, jako Ctrl+Cje .

Předchozí příklad pouze ukazuje, že čtení dat a přijímání dat blokuje operace. Pokud chcete tento problém vyřešit, můžete změnit kód na smyčku for a přijmout pouze data, která opravdu odesíláte, například v tomto příkladu:

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

Tady je konečná verze programu pro případ, že by se něco nepovedlo s vaší verzí:

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)

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

Po opětovném spuštění programu se zobrazí výstup podobný tomuto:

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://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!

Program dělá to, co má dělat. Už nepoužíváte funkci spánku, používáte kanály. Všimněte si také, že dokončení trvá přibližně 600 ms místo téměř 2 sekundy, když jsme nepouznali souběžnost.

Nakonec můžeme říci, že nepřipojené kanály synchronizují operace odesílání a příjmu. I když používáte souběžnost, komunikace je synchronní.