Informace o kanálech s vyrovnávací pamětí
Jak jste se dozvěděli, kanály jsou ve výchozím nastavení nepřečtené. To znamená, že přijímají operaci odeslání pouze v případě, že existuje operace příjmu. Jinak bude program zablokovaný čekání na věky.
Existuje čas, kdy potřebujete tento typ synchronizace mezi goroutinami. Někdy ale může docházet k tomu, že stačí implementovat souběžnost a nemusíte omezovat, jak spolu goroutiny komunikují.
Kanály ve vyrovnávací paměti odesílají a přijímají data bez blokování programu, protože se kanál ve vyrovnávací paměti chová jako fronta. Velikost této fronty můžete omezit při vytváření kanálu, například takto:
ch := make(chan string, 10)
Pokaždé, když něco odešlete do kanálu, prvek se přidá do fronty. Potom operace příjmu odebere prvek z fronty. Když je kanál plný, všechny operace odesílání jednoduše počká, dokud nebude k dispozici místo pro uložení dat. Pokud je naopak kanál prázdný a operace čtení je zablokovaná, dokud se něco nečte.
Tady je jednoduchý příklad, jak porozumět kanálům ve vyrovnávací paměti:
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!")
}
Při spuštění programu se zobrazí následující výstup:
All data sent to the channel ...
one
two
three
four
Done!
Můžete říct, že jsme tady neudělali nic jiného, a vy byste měli pravdu. Pojďme se ale podívat, kdy se proměnná změní size
na nižší číslo (můžete to dokonce zkusit s vyšším číslem), například takto:
size := 2
Při opětovném spuštění programu se zobrazí následující chyba:
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
Důvodem je, že volání send
funkce jsou sekvenční. Nevytvářeš novou goroutinu. Proto není nic, co by se zařadí do fronty.
Kanály jsou hluboce propojené s goroutinami. Bez dalšího goroutiny příjmu dat z kanálu může celý program vstoupit do bloku navždy. Jak jste viděli, děje se to.
Teď udělejme něco zajímavého! Vytvoříme goroutinu pro poslední dvě volání (první dvě volání se vejdou do vyrovnávací paměti správně) a spustíme smyčku for čtyřikrát. Tady je kód:
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!")
}
Když program spustíte, funguje podle očekávání. Doporučujeme, abyste při používání kanálů vždy používali goroutiny.
Pojďme otestovat případ, kdy vytvoříte kanál s vyrovnávací pamětí s více prvky, než budete potřebovat. Použijeme příklad, který jsme použili před kontrolou rozhraní API, a vytvoříme kanál s vyrovnávací pamětí s velikostí 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)
}
Při spuštění programu získáte stejný výstup jako předtím. Můžete si pohrát změnou velikosti kanálu s nižšími nebo vyššími čísly a program bude i nadále fungovat.
Nepřipojené vs. vyrovnávací paměti kanálů
V tuto chvíli vás může zajímat, kdy použít jeden typ nebo jiný. Vše závisí na tom, jak chcete, aby komunikace proudila mezi goroutinami. Nepřipojené kanály komunikují synchronně. Zaručuje, že pokaždé, když odesíláte data, program se zablokuje, dokud někdo nečte z kanálu.
Naopak kanály s vyrovnávací pamětí oddělují operace odesílání a příjmu. Nezablokují program, ale musíte být opatrní, protože může dojít k zablokování (jak jste viděli dříve). Pokud používáte nepřipojené kanály, můžete řídit, kolik goroutin může běžet souběžně. Můžete například volat rozhraní API a chcete řídit, kolik volání provádíte každou sekundu. Jinak může dojít k zablokování.
Pokyny pro kanály
Kanály v Go mají další zajímavou funkci. Pokud jako parametry pro funkci používáte kanály, můžete určit, jestli má kanál odesílat nebo přijímat data. S růstem programu můžete mít příliš mnoho funkcí a je vhodné zdokumentovat záměr každého kanálu, aby je správně používal. Nebo možná píšete knihovnu a chcete kanál zveřejnit jen pro čtení, abyste zachovali konzistenci dat.
Pokud chcete definovat směr kanálu, uděláte to podobným způsobem, jako když čtete nebo přijímáte data. Ale to uděláte, když deklarujete kanál v parametru funkce. Syntaxe pro definování typu kanálu jako parametru ve funkci je:
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data
Když odesíláte data prostřednictvím kanálu, který má být jen pro příjem, při kompilaci programu se zobrazí chyba.
Jako příklad dvou funkcí použijeme následující program, jeden, který čte data a druhý, který odesílá data:
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)
}
Při spuštění programu se zobrazí následující výstup:
Sending: "Hello World!"
Receiving: "Hello World!"
Program vysvětluje záměr každého kanálu ve všech funkcích. Pokud se pokusíte použít kanál k odesílání dat v kanálu, jehož účelem je pouze přijímat data, zobrazí se chyba kompilace. Zkuste například udělat něco takového:
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
ch <- "Bye!"
}
Při spuštění programu se zobrazí následující chyba:
# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)
Je lepší mít chybu kompilace než zneužití kanálu.
Multiplexování
Nakonec se podíváme, jak pracovat s více kanály současně pomocí klíčového select
slova. Někdy budete chtít počkat, až se událost stane, když pracujete s několika kanály. Můžete například zahrnout určitou logiku pro zrušení operace, když dojde k anomálii v datech, která váš program zpracovává.
Příkaz select
funguje jako switch
příkaz, ale pro kanály. Zablokuje provádění programu, dokud neobdrží událost ke zpracování. Pokud se zobrazí více než jedna událost, vybere ji náhodně.
Základním aspektem select
příkazu je, že po zpracování události dokončí její provádění. Pokud chcete počkat na další události, možná budete muset použít smyčku.
Pojďme použít následující program k zobrazení select
v akci:
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)
}
}
}
Při spuštění programu se zobrazí následující výstup:
Done replicating!
Done processing!
Všimněte si, že replicate
funkce byla dokončena jako první, a proto se nejprve zobrazí její výstup v terminálu. Hlavní funkce má smyčku, protože select
příkaz končí, jakmile přijme událost, ale stále čekáme na process
dokončení funkce.