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ć 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".

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