Úvod do Kotlin Coroutines

1. Prehľad

V tomto článku sa pozrieme na korutíny z jazyka Kotlin. Jednoducho povedané, coutiny nám umožňujú vytvárať asynchrónne programy veľmi plynulo, a sú založené na koncepcii Štýl pokračovania a odovzdania programovanie.

Jazyk Kotlin nám dáva základné konštrukty, ale môže získať prístup k užitočnejším programom pomocou jadro kotlinx-coroutines knižnica. Túto knižnicu si pozrieme, keď pochopíme základné stavebné prvky jazyka Kotlin.

2. Vytvorenie Coroutine pomocou BuildSequence

Vytvorme prvý korutín pomocou buildSequence funkcie.

A poďme implementovať generátor sekvencie Fibonacci pomocou tejto funkcie:

val fibonacciSeq = buildSequence {var a = 0 var b = 1 výnos (1) while (true) {yield (a + b) val tmp = a + b a = b b = tmp}}

Podpis a výnos funkcia je:

verejný abstrakt pozastaviť zábavný výnos (hodnota: T)

The pozastaviť kľúčové slovo znamená, že táto funkcia môže byť blokujúca. Takáto funkcia môže pozastaviť a buildSequence korutín.

Pozastavovacie funkcie môžu byť vytvorené ako štandardné Kotlinove funkcie, musíme si však uvedomiť, že ich môžeme volať iba v rámci procesu. V opačnom prípade sa zobrazí chyba kompilátora.

Ak sme prerušili hovor v rámci buildSequence, toto volanie sa transformuje do vyhradeného stavu v stavovom stroji. Program môže byť odovzdaný a priradený k premennej ako každá iná funkcia.

V fibonacciSeq coutín, máme dva závesné body. Najprv, keď voláme výnos (1) a druhá, keď voláme výťažok (a + b).

Ak to výnos funkcia vyústi do nejakého blokujúceho volania, súčasné vlákno sa na ňom nebude blokovať. Bude schopný vykonať ďalší kód. Akonáhle pozastavená funkcia dokončí svoje vykonávanie, vlákno môže pokračovať v spustení fibonacciSeq korutín.

Náš kód môžeme otestovať tak, že vezmeme niektoré prvky z Fibonacciho postupnosti:

val res = fibonacciSeq .take (5) .toList () assertEquals (res, listOf (1, 1, 2, 3, 5))

3. Pridanie závislosti Maven pre kotlinx-coutiny

Pozrime sa na kotlinx-coutiny knižnica, ktorá má užitočné konštrukcie postavené na základných rutinách.

Pridajme závislosť do jadro kotlinx-coroutines knižnica. Upozorňujeme, že musíme pridať aj znak jcenter Úložisko:

 org.jetbrains.kotlinx kotlinx-coroutines-core 0,16 centrálne //jcenter.bintray.com 

4. Asynchrónne programovanie pomocou spustenie () Coroutín

The kotlinx-coutiny knižnica pridáva množstvo užitočných konštrukcií, ktoré nám umožňujú vytvárať asynchrónne programy. Povedzme, že máme nákladnú výpočtovú funkciu, ktorá pridáva a String do vstupného zoznamu:

suspend fun expensiveComputation (res: MutableList) {delay (1000L) res.add ("word!")}

Môžeme použiť a spustenie korutín, ktorý vykoná túto funkciu pozastavenia neblokujúcim spôsobom - musíme k nej odovzdať oblasť vlákien ako argument.

The spustenie funkcia vracia a Job inštancia, na ktorej môžeme nazvať a pripojiť sa () metóda čakania na výsledky:

@Test zábava givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay () {// daný val res = mutableListOf () // keď runBlocking {val slib = launch (CommonPool) {drahýComputation (res)} res.add ("Hello,") sľub.join ()} / then assertEquals (res, listOf ("Hello,", "word!"))}

Aby sme mohli náš kód otestovať, odovzdáme všetku logiku do runBlocking coroutine - čo je blokujúci hovor. Preto náš assertEquals () možno vykonať synchrónne po kóde vo vnútri súboru runBlocking () metóda.

Upozorňujeme, že v tomto príklade, aj keď spustiť () metóda sa spustí ako prvá, jedná sa o oneskorený výpočet. Hlavné vlákno bude pokračovať pripojením súboru "Ahoj," reťazec do zoznamu výsledkov.

Po jednom sekundovom oneskorení, ktoré sa zavádza v drahý výpočet () funkcia, "Slovo!" String sa pripojí k výsledku.

5. Korutíny sú veľmi ľahké

Predstavme si situáciu, v ktorej chceme vykonať 100 000 operácií asynchrónne. Vytvorenie tak vysokého počtu vlákien bude veľmi nákladné a pravdepodobne prinesie OutOfMemoryException.

Našťastie to pri použití korutín nie je prípad. Môžeme vykonať toľko blokujúcich operácií, koľko chceme. Pod kapotou budú tieto operácie spracovávané pevným počtom vlákien bez nadmerného vytvárania vlákien:

@Test fun givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory () {runBlocking {// given val counter = AtomicInteger (0) val numberOfCoroutines = 100_000 // when val jobs = List (numberOfCoroutines) {launch (CommonPrint)) jobs.forEach {it.join ()} // potom assertEquals (counter.get (), numberOfCoroutines)}}

Všimnite si, že vykonávame 100 000 coututín a každé spustenie pridáva značné oneskorenie. Napriek tomu nie je potrebné vytvárať príliš veľa vlákien, pretože tieto operácie sa vykonávajú asynchrónnym spôsobom pomocou vlákna z Spoločný bazén.

6. Storno a časové limity

Niekedy potom, čo sme spustili nejaký dlhotrvajúci asynchrónny výpočet, chceme ho zrušiť, pretože nás výsledok už nezaujíma.

Keď začneme našu asynchrónnu činnosť s spustiť () coroutine, môžeme preskúmať jeAktívny vlajka. Tento príznak je nastavený na false vždy, keď hlavné vlákno vyvolá Zrušiť() metóda na inštancii Práca:

@Test fun givenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// given val job = launch (CommonPool) {while (isActive) {println ("is working")}} delay (1300L) // when job.cancel () // then cancel úspešne}}

Jedná sa o veľmi elegantný a jednoduchý spôsob použitia mechanizmu zrušenia. Pri asynchrónnej akcii stačí skontrolovať, či jeAktívny vlajka sa rovná nepravdivé a zrušiť naše spracovanie.

Keď požadujeme nejaké spracovanie a nie sme si istí, koľko času výpočet bude trvať, je vhodné nastaviť časový limit pre takúto akciu. Ak sa spracovanie v danom časovom limite nedokončí, dostaneme výnimku a môžeme na ňu primerane reagovať.

Napríklad môžeme akciu skúsiť znova:

@Test (očakávaný = CancellationException :: class) fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {repeat (1000) {i -> println ("Some expensive computation $ i ...") delay (500L)}}} }

Ak nedefinujeme časový limit, je možné, že naše vlákno bude navždy zablokované, pretože tento výpočet bude zablokovaný. Ak časový limit nie je definovaný, nemôžeme tento prípad vyriešiť v našom kóde.

7. Spustenie asynchrónnych akcií súčasne

Povedzme, že musíme spustiť dve asynchrónne akcie súčasne a potom počkať na ich výsledky. Ak naše spracovanie trvá jednu sekundu a musíme toto spracovanie vykonať dvakrát, bude runtime vykonania synchrónneho blokovania dve sekundy.

Bolo by lepšie, keby sme mohli obe tieto akcie spustiť v samostatných vláknach a počkať na tieto výsledky v hlavnom vlákne.

Môžeme využiť async () na dosiahnutie tohto cieľa spustením spracovania v dvoch samostatných vláknach súčasne:

@Test fun givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool) {someExpensiveComputation (delay)} val two = async (Common) runBlocking {one.await () two.await ()}} // potom assertTrue (time <delay * 2)}}

Po zadaní dvoch drahých výpočtov pozastavíme program vykonaním príkazu runBlocking () hovor. Raz výsledky jeden a dva sú k dispozícii, rutina sa obnoví a výsledky sa vrátia. Vykonanie dvoch úloh týmto spôsobom by malo trvať asi jednu sekundu.

Môžeme prejsť CoroutineStart.LAZY ako druhý argument k async () metóda, ale to bude znamenať, že asynchrónny výpočet sa nespustí, kým o to nebudete požiadaní. Pretože požadujeme výpočet v runBlocking coroutine, to znamená volanie na two.await () sa uskutoční iba raz one.await () skončil:

@Test zábava givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool, CoroutineStart.LAZY) {someExpensiveComputation (delay)} val = someExpensiveComputation (delay)} // when runBlocking {one.await () two.await ()}} // then assertTrue (time> delay * 2)}}

Lenivosť vykonania v tomto konkrétnom príklade spôsobí, že náš kód beží synchrónne. To sa stáva, pretože keď voláme čakať (), hlavné vlákno je blokované a iba po vykonaní úlohy jeden dokončí úlohu dva sa spustí.

Musíme si uvedomiť vykonávanie asynchrónnych akcií lenivým spôsobom, pretože môžu prebiehať blokujúcim spôsobom.

8. Záver

V tomto článku sme sa pozreli na základy kotlínskych rutín.

To sme videli buildSequence je hlavným stavebným prvkom každého korutínu. Opísali sme, ako vyzerá priebeh vykonávania v tomto štýle programovania prechádzajúceho pokračovaním.

Nakoniec sme sa pozreli na kotlinx-coutiny knižnica, ktorá dodáva veľa veľmi užitočných konštrukcií na vytváranie asynchrónnych programov.

Implementáciu všetkých týchto príkladov a útržkov kódu nájdete v projekte GitHub.