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 jedno rdzeniowy). 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, tworzone są nowe podejścia do tego zagadnienia. W Go mamy kolejny model który przypomina pythonowego stacklessa, czy też erlangowe micro procesy.
Żeby korzystać ze współbieżności potrzebujemy kilka mechanizmów: - określania jakie polecenia 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.
package main import "fmt" func goroutine() { fmt.Println("in goroutine") } func main() { go goroutine() }
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ć goroutinea. 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".
package main import "fmt" func goroutine() { fmt.Println("in goroutine") //1 } func main() { ch := make(chan bool) go goroutine() <- ch }
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.
package main import "fmt" func goroutine(c chan bool) { fmt.Println("in goroutine") c <- true } func main() { c := make(chan bool) go goroutine(c) <- c }
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ą:
package main import "fmt" func gofunc(c <- chan bool) { fmt.Println("gofunc") c <- true } func main() { c := main(chan bool) go gofunc(c) <- c }
Synchronizacja
Sposób działania kanałów wystarcza do synchronizacji stanu aplikacji. Warto w
tym temacie przypomnieć o instrukcji select, która wykonuje bloki kodu zależnie
od możliwości wykonania operacji na kanałach.
Wszystko o języku programowania Go