Współbieżność
Programy w których kod się wykonuje linia po linii, lub funkcja za funkcją powoli stają się rzadkością. Ciężko znaleźć nowy sprzęt komputerowy, który byłby wyposażony w pojedynczą jednostkę obliczeniową (procesor jednordzeniowy). Grzechem byłoby nie wykorzystanie dodatkowych mocy obliczeniowych w celu przyśpieszenia wykonania zadania jakie zostało postawione programowi. Robi się to zrównoleglając obliczenia.
Do niedawna były dwa główne narzędzia do tworzenia współbieżności: procesy i wątki. Ponieważ coraz częściej te modele są uważane za "ciężkie" i zbyt skomplikowane, próbowane są inne podejścia do tego zagadnienia. W Go mamy kolejny model który przypomina pythonowego stacklessa, czy też erlangowe microprocesy.
Żeby korzystać ze współbieżności potrzebujemy kilka mechanizmów: - określania jakie czynności mają być wykonane poza głównym przebiegiem programu - przekazywania danych wejściowych i odbierania danych wynikowych tego podprogramu - oczekiwania na zakończenie obliczeń równoległych
W Go te mechanizmy są realizowane za pomocą gorutyn (goroutines, nie mylić z coroutines), kanałów (chan) i operatorom "komunikacyjnym" oraz instrukcji select.
Goroutines
Gorutina to funkcja o równoległym wykonaniu (poza wątkiem ją wywołującym).
Gorutiną może zostać dowolna zdefiniowana funkcja w programie. Nie ma znaczenia
czy jest zwykłą funkcją, anonimową czy jest metodą. Aby ją uruchomić w trybie
równoległym, wystarczy przed wywołaniem dodać słówko go
.
Program się kompiluje i wszyscy są szczęśliwi poza tymi co uruchomili program.
Większości nie pojawi się napis, który miała wypisać goroutine
a. Jest to dowodem
na to, że funkcja goroutine została wywołana poza głównym wątkiem programu. Bo
programy w Go kończą swoje wykonanie, gdy zakończy się przetwarzanie funkcji
main
. Jak widać kończy się wcześniej niż funkcja goroutine
zdąży wyświetlić
na ekranie napis. To swoją drogą częsta pomyłka na początku przygody z Go i
współbieżnością. Aby poprawić powyższy program, aby jednak wyświetlał pożądany
napis, musimy lepiej poznać typy chan
.
Kanały
chan
to bardzo ciekawa rodzina typów. W normalnym programowaniu może być
użyteczna, ale jej prawdziwa siła ujawnia się w programowaniu współbieżnym.
Wartości typu chan
mają spełniać rolę komunikacyjną, kanałami właśnie
przekazujemy informacje między goroutines, możemy także wykorzystać je do
komunikacji między zwykłymi funkcjami, ale to wymaga trochę wyobraźni.
Przesyłanie informacji to proces w którym występuje nadawca i odbiorca, rolą nadawcy jest wysyłanie komunikatu a odbiorcy jego odbieranie. Truizm, ale ważny. Kanały pozwalają nadawcy wysłać komunikat, który gdzieś zostanie odebrany. Można je sobie wyobrażać jako rury, do których wkłada się informacje z jednej strony a wyciąga z drugiej. Tak jak konkretne rury służą do przesyłania określonych cieczy kanały służą do przesyłania informacji konkretnych typów i tak jak rury mają ograniczoną pojemność. Te dwie cechy mają odzwierciedlenie w deklaracjach typów, i tworzeniu wartości.
Tworzenie kanałów
type boolChan chan bool
Tak wygląda deklaracja typu kanału, którym przesyłamy wartości logiczne. Wartość
tego typu tworzymy używając funkcji make
ponieważ, tak jak slice
czy map
,
chan
to typ referencyjny.
var a boolChan = make(boolChan)
Tak utworzony kanał może przyjąć jedną wartość logiczną na raz, po czym się zatyka do czasu, wyciągnięcia z niego tej wartości, co więcej blokuje każdą gorutynę, która próbuje wysłać coś za jego pośrednictwem. Podobny mechanizm działa także w drugą stronę, tj. nie możemy nic wyciągnąć z pustego kanału i taka operacja blokuje wykonanie gorutyny w której jest podejmowane takie działanie. Tą drugą cechę niedługo wykorzystamy, by poprawić program z sekcji o gorutynach, ale najpierw nauczmy się wysyłać i odbierać informacje przez kanał.
Można też utworzyć kanały o większej pojemności, wystarczy podać ilość elementów
w drugim argumencie wywołania make
a100 := make(boolChan, 100)
Operacje na kanałach
Operator wysyłki i odbioru wygląda tak samo jest to znak mniejszości i minus
<-
, którą z tych operacji reprezentuje w danym momencie zależy od tego
czy operator znajduje się z prawej (wysyłka) czy z lewej (odbiór) strony zmiennej
będącej kanałem.
c := make(chan bool)
//wysyłka wartości true do kanału c
c <- true
//odbiór wartości z kanału i przypisanie jej do zmiennej b
b := <- c
Operacja odbierania wartości z kanału może być dwuwartościowa:
b, ok := <- c
if ok {
fmt.Println("Przyszło", b)
}
Druga wartość jest fałszem lub prawdą w zależności, czy kanał jest otwarty czy
zamknięty. Nowo utworzony kanał jest otwarty, można go zamknąć za pomocą wbudowanej
funkcji close
:
close(ch)
Skutkuje to tym, że próba wysłania przez kanał zakończy się rzuceniem błędu panic, a każde pobranie danych zwróci zero-typu wartości przesyłanych. Dlatego ważne jest, sprawdzanie statusu odbioru, najlepiej robić to zawsze bez wyjątku. Taki program zapętli się na zawsze:
package main
func main() {
a := make(chan bool);
close(a)
for {
<- a
}
}
Naprawianie początkowego przykładu
Skoro już wiemy jak wysyłać i odbierać wartości do i z kanału poprawmy nasz program tak by pokazywał się napis "in goroutine".
Jakby lepiej. Nasz program wyprowadzi na ekran napis "in goroutine", ale zakończy
się z hukiem pisząc o zakleszczeniu (deadlock). Dzieje się tak, gdy wszystkie
gorutyny (w tym główny przebieg programu) są zablokowane na operacjach komunikacji
kanałowej. A tak faktycznie jest bo gorutyna gorutine
kończy swoje wykonanie
po linii //1 i zostaje nam tylko gorutyna główna, która oczekuje na wyciągnięcie
wartości z pustego kanału ch.
Żeby uzyskać poprawny efekt, powinniśmy coś przekazać do kanału ch
, najlepszym
momentem by to zrobić jest chwila po wypisaniu oczekiwanego napisu.
Kanały jednostronne
Praca z kanałami jest przyjemna: zapewniają dwustronną komunikację między gorutynami bez potrzeby używania mutexów, lockowania i tego typu okropności, z których musieliśmy korzystać w językach do których współbieżność została dosztukowana. Problem jaki może się pojawić to wykorzystanie kanału do wysyłki w gorutynie która odbiera (lub w zamyśle miała tylko odbierać) informacje, co może powodować zapętlenia.
W Go jest możliwość deklarowania typu kanału tylko do wysyłki, lub tylko do odbioru. W praktyce najczęściej robi się to w sygnaturze funkcji, która ma być gorutyną:
Synchronizacja
Blokowanie goroutyny na operacjach wysyłania i odbierania z kanału często
wystarcza aby zsynchronizować stan aplikacji. Warto w tym temacie przypomnieć
o instrukcji select
, która wykonuje operacje zależnie od możliwości wykonania
operacji na kanałach.
Nie mniej, czasem wygodniej jest skorzystać z mutexów, lub innych blokad. Można
je znaleźć w pakiecie sync