Problem
Tradycyjny model tworzenia kodu asynchronicznego opartego o metody zwrotne (Callback
) narażony jest na występowanie różnych trudności. W przypadku realizacji współbieżnych zadań zależnych może pojawić się problem komunikacji między zadaniami, który wymusza znalezienie sposobu na synchronizowanie rezultatów zwiększając tym samym złożoność kodu. Innym problemem jest zagnieżdżenie metod zwrotnych co z kolei sprawia, że w rzeczywistości zadania wykonywane są synchronicznie przez co wydłuża się czas ich przetwarzania. Coroutines
(współprogramy) znacznie ułatwiają realizację zadań współbieżnych poprzez zmianę stylu pisania kodu asynchronicznego w sposób sekwencyjny. Dzięki takiemu podejściu kod jest bardziej czytelny i zrozumiały, a zarządzanie zadaniami staję się łatwiejsze. Ponadto jeden wątek potrafi obsługiwać jednocześnie wiele coroutines co przekłada się na znaczny wzrost wydajności. O coroutine
można myśleć jako sekwencji podzadań wykonywanych wg określonej kolejności.
Funkcje zawieszania
Zasada działania coroutine oparta jest o ideę funkcji zawieszania (suspend function
). Funkcja ta potrafi zawiesić swoje działanie do późniejszego wykonania bez blokowania wątku (np. w oczekiwaniu na zakończenie innej funkcji), gdzie koszt zawieszenia w stosunku do blokowania wątku juz dużo niższy. Funkcje wstrzymania muszą być wywołane wewnątrz coroutine lub w innej funkcji zawieszenia i mogą być uruchamiane na tym samym lub różnych wątkach. Aby zadeklarować funkcje zawieszenia należy użyć słowa kluczowego suspend
.
Kontekst
Kontekst współprogramu (CoroutineContext
) jest zbiorem zasad i konfiguracji, która definiuje sposób w jaki coroutine będzie wykonywany. Może być także kombinacją obiektów różnych typów kontekstu (CombinedContext
). Składa się przeważnie z obiektów typu Job
, CoroutineDispatcher
oraz CoroutineExceptionHandler
. Coroutine zawsze wykonywany jest w ramach jakiegoś kontekstu, który może być przekazany jawnie jako argument lub uzyskiwany niejawnie na podstawie zakresu w którym jest wykonywany.
Dispatcher
Kontekts zawiera m.in. instancję CoroutineDispatcher
, której zadaniem jest określenie wątków wykonawczych dla coroutine. Dispatchers.Default
może działać na wielu wątkach i używany jest do kosztownych zadań o dużym zapotrzebowaniu mocy obliczeniowej jak np. algorytmy. Dispatchers.Main
działa na głównym wątku co w przypadku Android pozwala na modyfikację interfejsu użytkownika. Dispatchers.IO
jest używany przede wszystkim do prostych operacji typu wejście/wyjście jak np. zapytanie sieciowe, dostęp do bazy danych, plików czy sensorów. Dispatchers.Unconfined
nie ogranicza się do żadnego wątku w wyniku czego jego zachowanie jest trudne do przewidzenia.
Budowniczy
Tworzenie i wykonanie coroutine może odbywać się przez jedną z funkcji budowniczego (coroutine builder
) do której należą m.in.: runBlocking
, launch
, async
. Funkcję te nie są funkcjami zawieszenia w związku z czym po wywołaniu kontynuowane jest działanie kolejnych instrukcji. Coroutines budowane są i działają w ramach struktury hierarchii (structured concurrency
), tzn. rodzic (parent coroutine
) ma zdolność do oddziaływania na cykl życia dzieci (child coroutine
), a ich wykonanie przywiązane jest do danego zakresu (scope
).
runBlocking
blokuje bieżący wątek dopóki wszystkie zadania w coroutine nie zostaną wykonane. Jest to przydatne w pisaniu testów wymagających wstrzymania.
launch
jest często wykorzystywanym budowniczym, który w przeciwieństwie do runBlocking
nie blokuje bieżącego wątku. Zwraca obiekt typu Job
dzięki któremu możliwe jest manualne zarządzanie stanem zadań. Metoda join
blokuje powiązany coroutine tak długo dopóki wszystkie jego zadania nie zostaną wykonane, natomiast cancel
anuluje wszystkie zadania. Wykorzystywany do zadań typu fire and forget
w których nie oczekuje się zwrócenia rezultatu.
async
podobnie jak launch
pozwala na równoległe wykonanie zadań w tle, jednakże zwraca obiekt typu Deferred
, który jest obietnicą przyszłego rezultatu. Metoda await
w przypadku braku wyniku zawiesza dalsze wykonywanie instrukcji do momentu otrzymania rezultatu.
Anulowanie
Wszystkie funkcje zawieszenia w coroutine są cancellable
, tzn. potrafią obsłużyć żądanie o anulowaniu pracy przez metodę cancel
. Jeśli coroutine został anulowany to wyrzucany jest wyjątek CancellationException
w wyniku czego następuje przerwanie działania. Jednakże jeśli blok kodu nie jest elementem funkcji zawieszenia wówczas nie dochodzi do automatycznego sprawdzania stanu pracy co sprawia, że kod nie reaguje na żądanie cancel
. W takiej sytuacji należy ręcznie sprawdzać stan pracy poprzez właściwość isActive
lub funkcję yield
(okresowo zawiesza działanie funkcji) czy też ustawienie maksymalnego czasu wykonania za pomocą funkcji withTimeout
i withTimeoutOrNull
.
Zakres
Coroutines działają w ramach zakresu (scope
), który stanowi dla nich przestrzeń wykonawczą i jest realizacją struktury hierarchii. Odwołanie się do zakresu wpływa na wszystkie znajdujące się w nim coroutines. Ich wykorzystanie eliminuje problem manualnego zarządzania stanem zadań, których praca i oczekiwanie na rezultat nierzadko ma sens tylko dla bieżącego ekranu (np. ładowanie danych do wyświetlenia). Zamiast ręcznego anulowania wszystkich coroutines wystarczy odwołać je poprzez zakres. Dowolna klasa implementująca CoroutineScope
oraz nadpisująca właściwość coroutineContext
może stać się zakresem. Warto zauważyć, że funkcje budowniczego są funkcjami rozszerzającymi CoroutineScope
. Przykładem zakresu może być GlobalScope
stanowiący ogólny zakres aplikacji.
Kanały
Kanały są mechanizmem (podobnym do kolejki) pozwalającymi na przesyłanie i odbieranie potoku strumienia wartości między coroutines. W celu zbudowania kanału należy stworzyć instancję klasy Channel
, która implementuje interfejsy zachowania zarówno nadawcy (SendChannel
) jak i odbiorcy (ReceiveChannel
). Opcjonalny parametr przekazany do metody wytwórczej odpowiada za wielkość bufora. Kanały bez buforowe przesyłają elementy dopiero wtedy gdy nadawca i odbiorca są gotowi do komunikacji (spotykają się), tzn. funkcja send
oczekuje na odbiorcę aby dokonać emisji natomiast funkcja receive
oczekuje na nadawcę aby rozpocząć odbieranie. W przypadku kanałów buforowych strategia zawieszenia pozwala na wcześniejszą emisję w zależności od wielkościu bufora. Przetwarzanie wartości może także odbywać się poprzez iteracje kanału lub funkcje consume
, consumeEach
.
Kanały typu Channel
ograniczone są do jednorazowego przepływu informacji, tzn. kanał może tylko raz odebrać wiadomości (w jednym miejscu). W sytuacji, gdy występuje wielu potencjalnych odbiorców należy użyć kanału typu BroadcastChannel
lub ConflatedBroadcastChannel
. Różnica między nimi polega na tym, że ten drugi informuje tylko o ostatniej wartości.
Możliwe jest także tworzenie coroutine z automatycznie załączonym kanałem nastawionym na emisje lub odbiór za pomocą funkcji budowniczych producenta i aktora. produce
uruchamia coroutine, który emituje strumień danych do kanału (jeden nadawca, wielu odbiorców). Zwraca ReceiveChannel
oraz należy do zakresu ProducerScope
.
actor
uruchamia coroutine, który odbiera wiadomości z kanału (jeden odbiorca, wielu nadawców). Zwraca SendChannel
i należy do zakresu ActorScope
.
Wyrażenie select
umożliwia jednoczesne oczekiwanie na wiele funkcji zawieszenia i wybranie pierwszej, która stanie się dostępna. Metoda onReceive
definiuje zachowania otrzymania wiadomości przez kanał, natomiast onSend
dokonuje emisji wartości do kanału.