Czym są korutyny w Kotlinie?

Radosław Kondziołka
Ikona kalendarza
17 grudnia 2020

Wraz z wersją 1.3 do Kotlina zostały wprowadzone korutyny. Sama koncepcja korutyn nie jest żadną nowinką w świecie programowania i występuje już w wielu językach. Korutyny w pewnym sensie oferują nieco inne podejście do programowania współbieżnego.

Pierwsza korutyna

W Kotlinie korutyny tworzy się przy pomocy tzw. coroutine builders, których jednym z reprezentantów jest użyty poniżej launch. Funkcja ta tworzy korutynę, w ramach której wykonywany jest blok kodu przekazany jako parametr:

1fun main(args: Array<String>) {
2    runBlocking {
3        launch {
4            delay(1000)
5        	println("The first coroutine was executed")
6    	  }
7    }
8}

kotlin runBlocking pełni tutaj rolę pewnego ułatwienia i nie należy się na nim skupiać. Na tym etapie wystarczy nam wiedza, że umożliwia on wykonanie korutyny w bieżącym wątku. Tak stworzona korutyna czeka sekundę, następnie wypisuje tekst na ekran, po czym kończy swoje działanie. Korutyny często określane są jako light thread co ma wskazywać na lekkość ich tworzenia i wykonywania. Podobnie do funkcji, korutyny są wykonywane w ramach wątku, przy czym jeden wątek może “jednocześnie” wykonywać wiele korutyn. Z każdą korutyną jest związany kontekst (CoroutineContext). I to od niego zależy, w którym wątku zostanie wykonana korutyna.

Kontekst korutyny

Korutyny zawsze działają w jakimś kontekście. Jak już wspomniano, kontekst określa, w jaki sposób korutyna zostanie wykonana oraz utrzymuje referencję do joba (Coroutine Job) , który reprezentuje wykonywany w ramach korutyny blok kodu. Posługując się jobem, który zostaje zwrócony przez funkcję tworzącą, można anulować wykonanie korutyny:

1runBlocking {
2    val job = launch {
3        println("Starting")
4        delay(1000)
5        println("Exiting")
6    }
7    delay(500)
8    job.cancel()
9}

Na ekranie został wypisany tylko pierwszy komunikat, ponieważ korutyna została anulowana.

Innym ważnym elementem kontekstu jest Dispatcher, który określa wątek wykonujący korutynę. Kotlin dostarcza kilka dispatcherów o różnych zastosowaniach. Jednym z takich dispatcherów jest kotlin Dispatchers.IO, który przeznaczony jest głównie do wykonywania operacji I/O. Poniżej pokazano, jak można określić, którego Dispatchera chcemy użyć do wykonania naszej korutyny:

1runBlocking {
2    launch(Dispatchers.IO) {
3        println("A coroutine on thread ${Thread.currentThread().name}")
4    }
5}
6

Jeżeli nie określimy kontekstu w funkcji tworzącej, to jest on dziedziczony z bieżącego zakresu, o którym więcej poniżej.

Zakres korutyny

kotlin CoroutineScope jest w pewnym sensie sposobem na strukturyzację wykonania korutyn. Zakresy mają strukturę hierarchiczną, tzn. istnieje między nimi relacja rodzic-dziecko. Korutyny działające w zakresie będącym w roli rodzica, zanim zakończą swoją pracę, czekają na zakończenie korutyn, które działają w zakresie będącym w roli dziecka. Hierarchiczna relacja między korutynami (a w rzeczywistości między jobami powiązanymi z korutynami) umożliwia propagowanie operacji anulowania od rodziców do dzieci. Poniżej krótki przykład pokazujący strukturę hierarchiczną między dwiema korutynami:

1GlobalScope.launch {
2    println("I am a parent coroutine")
3    launch {
4        println("I am a child coroutine")
5        delay(1000)
6    }.invokeOnCompletion { println("Completion of a child coroutine") }
7}.invokeOnCompletion { println("Completion of a parent coroutine") }

Powyżej, zewnętrzna korutyna została umieszczona w tzw. GlobalScope, który jest globalnym zakresem w aplikacji. Korutyna wewnętrzna dziedziczy kontekst z korutyny zewnętrznej oraz staje się dzieckiem korutyny zewnętrznej. Wynik powyższego programu.

1I am a parent coroutine
2I am a child coroutine
3Completion of a child coroutine
4Completion of a parent coroutine

zdradza konsekwencję takiego stanu rzeczy: korutyna zewnętrzna jako rodzic czeka na zakończenie korutyny-dziecka.

Korutyny, czym właściwie są?

Na tym etapie nie wygląda na to, żeby wykonanie korutyn jakoś szczególnie odstawało od wykonania zwykłych funkcji. W gruncie rzeczy tak właśnie jest, z tą różnicą, że wykonanie korutyn może być wstrzymywane w specjalnych punktach - tzw. suspension points. Takie wstrzymanie polega na przerwaniu wykonywania w obecnym wątku bieżącej korutyny, ale w sposób nieblokujący wątku, który tą korutynę wykonuje. Taki wątek może zająć się na przykład wykonywaniem kodu innej korutyny. Wykonywanie tak przerwanej korutyny będzie potem kontynuowane od punktu wstrzymania. Spójrzmy poniżej na poniższy kod i rezultat jego wykonania rezultat:

1runBlocking {
2    launch {
3        println("Start #1 on thread ${Thread.currentThread().name}")
4        yield() // suspension point
5        println("Exit #1 on thread ${Thread.currentThread().name}")
6   }
7   launch {
8        println("Start #2 on thread ${Thread.currentThread().name}")
9        println("Exit #2 on thread ${Thread.currentThread().name}")
10   }
11}
1Start #1 on thread main
2Start #2 on thread main
3Exit #2 on thread main
4Exit #1 on thread main

W tym przypadku rolę punktu wstrzymującego pełni funkcja kotlin yield. Zaobserwowany przebieg jest następujący:

  1. Wykonana została pierwsza instrukcja z pierwszej korutyny.
  2. Korutyna w punkcie wstrzymania zostaje wstrzymana.
  3. Druga korutyna zostaje w całości wykonana.
  4. Wstrzymana wcześniej korutyna zostaje wznowiona.

Jasno wskazuje to na "przełączenie kontekstu", które nastąpiło w ramach jednego wątku. Warto tutaj zaznaczyć, że - generalnie - dobrymi kandydatami na takie punkty wstrzymania są operacje, które z natury są operacjami blokującymi, np. komunikacja sieciowa. Przykładowo, w momencie wykonywania zapytania sieciowego, korutyna zostaje wstrzymana i na bieżącym wątku może być wykonywana inna korutyna. Nie oznacza to wcale, że zapytanie sieciowe nie jest wykonywane. Jego wykonanie mogło zostać zaimplementowane w sposób nieblokujący bądź zostać przesunięte na inny wątek. Punkty wstrzymania mogą być umieszczane albo bezpośrednio w ciele korutyn, albo w suspend functions, którym przyjrzymy się w następnej sekcji.

Suspend functions

Suspend functions to funkcje, których wykonanie może być wstrzymywane w pewnych punktach (suspension points) i potem kontynuowane. Funkcje tego typu mogą być wykonywane tylko w obrębie korutyny. Można powiedzieć, że funkcje wstrzymywane są podstawowym budulcem, z którego zbudowana jest korutyna a korutyna pełni rolę "wykonawcy" względem tych funkcji. W Koltinie definicję takich funkcji wprowadza się przy pomocy dedykowanego słowa kluczowego suspend.

1suspend fun prepareAgreement(templateId: String, customer: Customer)
2    : Agreement { 
3    val request = prepareRequest()
4    val template = client.getTemplate(request) // do HTTP GET request (suspension post)
5    return createAgreement(template, customer)
6}

Poniżej została przedstawiona grafika pokazująca przebieg wykonania takiego programu:

obraz1.webp

Zielonym kolorem oznaczono funkcję prepareAgreement natomiast niebieskim jakąś inną korutynę. W trakcie pobierania szablonu umowy z innego serwisu wątek T1 wykonuje inną korutynę nie marnując w ten sposób zasobów. Zakładamy tutaj, że korzystamy z biblioteki, która potrafi wykonywać zapytania HTTP w sposób nieblokujący.

Podsumowanie

Programowanie przy użyciu korutyn może wydawać się na pozór bardzo łatwe. Należy jednak być przy tym bardzo uważnym, gdyż niewłaściwie zaimplementowana korutyna może wpływać negatywnie na wykonanie pozostałych korutyn z tego samego kontekstu, co może skutkować na przykład obniżoną responsywnością aplikacji. Nie mniej jednak umiejętność programowania w oparciu o korutyny jest bardzo przydatną umiejętnością w warsztacie programisty.

Przeczytaj także

Ikona kalendarza

29 marzec

To be or not to be a multi cloud company? Przewodnik dla kadry kierowniczej, doradców ds. chmury i architektów IT. (część 2)

Po przeczytaniu pierwszej części poradnika zauważymy, że strategie organizacji są różne. Część firm oparło swój biznes wyłącznie na j...

Ikona kalendarza

27 wrzesień

Sages wdraża system Omega-PSIR oraz System Oceny Pracowniczej w SGH

Wdrożenie Omega-PSIR i Systemu Oceny Pracowniczej w SGH. Sprawdź, jak nasze rozwiązania wspierają zarządzanie uczelnią i potencjałem ...

Ikona kalendarza

12 wrzesień

Playwright vs Cypress vs Selenium - czy warto postawić na nowe?

Playwright, Selenium czy Cypress? Odkryj kluczowe różnice i zalety każdego z tych narzędzi do automatyzacji testów aplikacji internet...