Dowiedz się więcej o buforowanych kanałach
Jak już wiesz, kanały są domyślnie niebuforowane. Oznacza to, że akceptują operację wysyłania tylko wtedy, gdy istnieje operacja odbierania. W przeciwnym razie program będzie czekał na zawsze.
Istnieją czasy, w których potrzebny jest ten typ synchronizacji między goroutines. Jednak mogą wystąpić czasy, w których po prostu trzeba zaimplementować współbieżność i nie trzeba ograniczać sposobu komunikowania się ze sobą goryiny.
Buforowane kanały wysyłają i odbierają dane bez blokowania programu, ponieważ buforowany kanał zachowuje się jak kolejka. Rozmiar tej kolejki można ograniczyć podczas tworzenia kanału w następujący sposób:
ch := make(chan string, 10)
Za każdym razem, gdy wysyłasz coś do kanału, element jest dodawany do kolejki. Następnie operacja odbierania usuwa element z kolejki. Gdy kanał jest zapełniony, każda operacja wysyłania po prostu czeka na miejsce do przechowywania danych. Z drugiej strony, jeśli kanał jest pusty i istnieje operacja odczytu, jest blokowany, dopóki nie będzie coś do odczytania.
Oto prosty przykład, aby zrozumieć buforowane kanały:
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!")
}
Po uruchomieniu programu zobaczysz następujące dane wyjściowe:
All data sent to the channel ...
one
two
three
four
Done!
Możesz powiedzieć, że nie zrobiliśmy nic innego tutaj i byłeś w porządku. Zobaczmy jednak, kiedy zmienisz size
zmienną na niższą liczbę (możesz nawet spróbować użyć większej liczby), tak jak poniżej:
size := 2
Po ponownym uruchomieniu programu zostanie wyświetlony następujący błąd:
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
Przyczyną jest to, że wywołania send
funkcji są sekwencyjne. Nie tworzysz nowej goroutiny. Dlatego nie ma nic do kolejki.
Kanały są głęboko połączone z goroutines. Bez innego goroutine odbierającego dane z kanału, cały program może wejść do bloku na zawsze. Jak już widzieliście, tak się dzieje.
Teraz zróbmy coś interesującego! Utworzymy goroutine dla dwóch ostatnich wywołań (pierwsze dwa wywołania pasują do buforu prawidłowo) i wykonamy uruchomienie pętli for cztery razy. Oto kod:
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!")
}
Po uruchomieniu programu działa zgodnie z oczekiwaniami. Zalecamy, aby w przypadku korzystania z kanałów zawsze używać goroutines.
Przetestujmy przypadek, w którym utworzysz buforowany kanał z większą ilością elementów, niż będziesz potrzebować. Użyjemy przykładu użytego wcześniej do sprawdzenia interfejsów API i utworzenia buforowanego kanału o rozmiarze 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)
}
Po uruchomieniu programu uzyskasz te same dane wyjściowe co poprzednio. Możesz obejść się, zmieniając rozmiar kanału z niższymi lub wyższymi liczbami, a program nadal będzie działać.
Kanały niebuforowane a buforowane
W tym momencie możesz się zastanawiać, kiedy użyć jednego typu lub innego. Wszystko zależy od tego, jak chcesz, aby komunikacja przepływła między goroutines. Kanały niebuforowane komunikują się synchronicznie. Gwarantują one, że za każdym razem, gdy wysyłasz dane, program zostanie zablokowany do momentu odczytania przez kogoś z kanału.
Z drugiej strony buforowane kanały oddzielają operacje wysyłania i odbierania. Nie blokują one programu, ale trzeba uważać, ponieważ może skończyć się zakleszczeniem (jak widzieliśsz wcześniej). W przypadku korzystania z niebuforowanych kanałów można kontrolować, ile goroutin może być uruchamianych współbieżnie. Możesz na przykład wykonywać wywołania interfejsu API i kontrolować liczbę wywołań, które wykonujesz co sekundę. W przeciwnym razie może zostać zablokowane.
Wskazówki dotyczące kanału
Kanały w języku Go mają inną interesującą funkcję. Gdy używasz kanałów jako parametrów do funkcji, możesz określić, czy kanał ma wysyłać lub odbierać dane. W miarę rozwoju programu może istnieć zbyt wiele funkcji i dobrym pomysłem jest udokumentowanie intencji każdego kanału w celu ich prawidłowego użycia. A może piszesz bibliotekę i chcesz uwidocznić kanał jako tylko do odczytu, aby zachować spójność danych.
Aby zdefiniować kierunek kanału, należy to zrobić w podobny sposób, jak podczas odczytywania lub odbierania danych. Jednak robisz to podczas deklarowania kanału w parametrze funkcji. Składnia służąca do definiowania typu kanału jako parametru w funkcji to:
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data
Podczas wysyłania danych za pośrednictwem kanału przeznaczonego tylko do odbierania występuje błąd podczas kompilowania programu.
Użyjmy następującego programu jako przykładu dwóch funkcji, które odczytują dane i drugą, która wysyła dane:
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)
}
Po uruchomieniu programu zobaczysz następujące dane wyjściowe:
Sending: "Hello World!"
Receiving: "Hello World!"
Program wyjaśnia intencję każdego kanału w każdej funkcji. Jeśli spróbujesz użyć kanału do wysyłania danych w kanale, którego celem jest tylko odbieranie danych, zostanie wyświetlony błąd kompilacji. Na przykład spróbuj wykonać następujące czynności:
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
ch <- "Bye!"
}
Po uruchomieniu programu zostanie wyświetlony następujący błąd:
# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)
Lepiej jest mieć błąd kompilacji niż niewłaściwe użycie kanału.
Multipleksowanie
Na koniec zobaczmy, jak korzystać z więcej niż jednego kanału jednocześnie przy użyciu słowa kluczowego select
. Czasami warto poczekać na zdarzenie, gdy pracujesz z wieloma kanałami. Na przykład możesz uwzględnić pewną logikę, aby anulować operację w przypadku wystąpienia anomalii w danych przetwarzanych przez program.
Instrukcja select
działa jak switch
instrukcja, ale dla kanałów. Blokuje wykonywanie programu do momentu odebrania zdarzenia do przetworzenia. Jeśli otrzyma więcej niż jedno zdarzenie, wybierze je losowo.
Istotnym aspektem select
instrukcji jest to, że kończy wykonywanie po jego przetwarzaniu zdarzenia. Jeśli chcesz poczekać na więcej zdarzeń, może być konieczne użycie pętli.
Użyjmy następującego programu, aby zobaczyć select
działanie:
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)
}
}
}
Po uruchomieniu programu zobaczysz następujące dane wyjściowe:
Done replicating!
Done processing!
Zwróć uwagę, że replicate
funkcja została ukończona jako pierwsza, dlatego najpierw w terminalu są widoczne jego dane wyjściowe. Funkcja main ma pętlę, ponieważ select
instrukcja kończy się natychmiast po odebraniu zdarzenia, ale nadal czekamy na process
zakończenie funkcji.