9. Apr 2021Android

Úvod do Kotlin Coroutines

Coroutiny sa stávajú bežnou technologiou pri vývoji mobilných aplikácií. Mnohí z nás už tento koncept poznajú alebo sa s ním stretli. Avšak stále nám chýba fundementálne pochopenie.

Tomáš ParonaiAndroid Developer

Routine vs Coroutine

Každému developerovi by malo byť jasné, čo znamená slovíčko routine. Routine je základná stavebná jednotka každého programu. Je to súbor inštrukcií, ktoré vykonávajú úlohy, tiež známy pod pojmami ako funkcia alebo metóda. Príkazy sa vykonávajú sekvenčne zhora nadol. V praxi to voláme synchrónne programovanie.

fun main() 
    println("main starts")
    routine(1, 500)
    routine(2, 300)
    println("main ends")
}

private fun routine(number: Int, delay: Long) {
    println("Routine $number starts to work")
    Thread.sleep(delay)
    println("Routine $number finished")
}

V tomto konkrétnom príklade môžeme vidieť, ako z neho prebieha nasledovný výstup. Funkcia routine predstavuje prácu, ktorú program vykonáva a trvá delay ms dlho. Druhá routine čaká 500ms na prvú, kým skončí. Routine 1 blokuje vlákno, kým neskončí. Takáto inštrukcia je známa pod pojmom blocking.

main starts
Routine 1 starts to work
Routine 1 finished
Routine 2 starts to work
Routine 2 finished
main ends

Príklad zobrazený pomocou diagramu:

coroutines diagram

V praxi nesenkvenčné programovania voláme asynchrónne. V tomto prípade sa inštrukcie jedného programu vykonávajú súbežne. Asynchrónne programovanie je zvlášť dôležité v Androide. Predstavme si situáciu, kedy používateľ v aplikácií čaká, kým sa stiahnu obrázky. Ak by sa sťahovali obrázky na rovnakom vlákne, na ktorom beží celé užívateľské rozhranie, celé rozhranie by stálo a čakalo, kým sa inštrukcie na stiahnutie a uloženie obrázkov dokončia. Aplikácia by sa javila ako pokazená a viedlo by to k veľmi nepríjemnému užívateľskému zážitku.

Najčastejším riešením by bolo použiť nové vlákno - Thread alebo RxJava. Všetky tieto spôsoby majú svoje výhody aj nevýhody. Skúsme si ukázať predchádzajúci príklad asynchrónne.

Thread

fun main() {
    println("main starts")
    thread { routine(1, 500) }
    thread { routine(2, 300) }
    Thread.sleep(600)
    println("main ends")
}

RxJava

fun main() {
    println("main starts")
    val disposables = CompositeDisposable()
    disposables.addAll(
        Completable.fromAction {
            routine(1, 500)
        }.subscribeOn(Schedulers.newThread()).subscribe(),
        Completable.fromAction {
            routine(2, 300)
        }.subscribeOn(Schedulers.newThread()).subscribe()
    )
    Thread.sleep(600)
    println("main ends")
    disposables.dispose()
}

V oboch prípadoch dosiahneme nasledujúci výstup:

main starts
Routine 1 starts to work
Routine 2 starts to work
Routine 2 finished
Routine 1 finished
main ends

Ako je z výstupu jasné, oba príkazy sa spustili jeden za druhým, teda nečakali na seba, ale bežali súbežne. Druhá routina je rýchlejšia, čiže skončila skôr. Takéto inštrukcie nazývame nonblocking.

multithread visualization via diagram

Pri vláknach nemáme kontrolu nad novými vláknami. Ak si predstavíme, že main je bežiaca Android aplikácia a my ju vypneme po zavolaní routine, tak nám vzniká memory leak.

Pri RxJave tento problém nemáme, lebo disponujeme naše inštrukcie po skončení programu, ale pri samotnom frameworku je potrebné si pamätať veľké množstvo inštrukcií, ktoré s multithredingom pracujú. Ak by sme v main funkcii nezavolali Thread.sleep(600), výpis "main ends" by sa vypísal predtým, ako by skončili naše routine funkcie a to z dôvodu, že naše routine funkcie bežia na inom vlákne ako main.

Coroutine

Z názvu coroutine si vieme už predstaviť, že nejde o bežný súbor inštrukcií, ktoré sa vykonávajú sekvenčne.

fun main() = runBlocking {
    println("main starts")
    joinAll(
        async { coroutine(1, 500L) },
        async { coroutine(2, 300L) }
    )
    println("main ends")
}

private suspend fun coroutine(number: Int, t: Long) {
    println("Routine $number starts to work")
    delay(t)
    println("Routine $number finished")
}

Na prvý pohľad sa môže zdať, že na predchádzajúcom obrázku je niekoľko nových neznámych príkazov. Príkaz runBlocking blokuje vlákno, na ktorom príkaz beží a spustí novú coroutinu. runBlocking voláme preto, lebo sa program sa vykonáva zhora nadol, ak chceme niekde vsunúť coroutinu a zároveň chceme, aby program nepokračoval, kým coroutina neskončí. Je to jeden z konštruktorov pre coroutiny.

Ďalším konštruktom coroutiny je launch, ktorý ju aj spúšťa. O všetkých konštruktoroch a rozdieloch medzi nimi vám poviem v ďalšom článku. Tieto coroutiny vkladáme do joinAll, od ktorej exekúcia príkazov nepokračuje, kým sa nevyhodnotia všetky coroutiny. Funkcia coroutine je vskutku to isté ako routine z predchádzajúceho príkladu. Nazvali sme to konvenčne. Ak spustíme tento príklad, dostaneme nasledujúci výsledok.

main starts
Routine 1 starts to work
Routine 2 starts to work
Routine 2 finished
Routine 1 finished
main ends

Jeden z najväčších rozdielov medzi multithreadingom a kotlin coroutinami je, že coroutiny bežia na rovnakom vlákne, na ktorom boli vytvorené. Pre pravdivosť tohto výroku, skúste si vypísať názov vlákna vo funkcií *routine* a *coroutine - Thread.currentThread().name.*

Aký je teda princíp? Vytvorenie a manažovanie ďalšieho vlákna je náročné na pamäť aj CPU. Takto vyzerá práca coroutiny na diagrame.

Uľahčuje sa nám najmä čitateľnosť kódu, lebo nie je potrebné vkladať callbacky ak by sme mali návratovú hodnotu. V ďalšom článku si ukážeme jednoduché použitie všetkých troch spôsobov asynchrónneho programovania pri komunikácií so serverom alebo s databázou.

Zaujíma vás Android? Navštívte článok o Constraint Layout Helpers

Tomáš ParonaiAndroid Developer