Inleiding tot Kotlin Coroutines

1. Overzicht

In dit artikel zullen we coroutines uit de Kotlin-taal bekijken. Simpel gezegd, coroutines stellen ons in staat om op een zeer vloeiende manier asynchrone programma's te maken, en ze zijn gebaseerd op het concept van Vervolgstijl programmeren.

De Kotlin-taal geeft ons basisconstructies, maar kan toegang krijgen tot meer bruikbare coroutines met de kotlinx-coroutines-core bibliotheek. We zullen deze bibliotheek bekijken zodra we de basisbouwstenen van de Kotlin-taal begrijpen.

2. Een Coroutine maken met BuildSequence

Laten we een eerste coroutine maken met de buildSequence functie.

En laten we een Fibonacci-reeksgenerator implementeren met behulp van deze functie:

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

De handtekening van een opbrengst functie is:

openbare samenvatting plezieropbrengst opschorten (waarde: T)

De opschorten trefwoord betekent dat deze functie kan worden geblokkeerd. Een dergelijke functie kan een buildSequence coroutine.

Suspending-functies kunnen worden gemaakt als standaard Kotlin-functies, maar we moeten ons ervan bewust zijn dat we ze alleen vanuit een coroutine kunnen aanroepen. Anders krijgen we een compilatiefout.

Als we het gesprek binnen de buildSequence, die oproep zal worden getransformeerd naar de toegewezen status in de statusmachine. Een coroutine kan worden doorgegeven en toegewezen aan een variabele zoals elke andere functie.

In de fibonacciSeq coroutine hebben we twee ophangpunten. Ten eerste, als we bellen opbrengst (1) en ten tweede wanneer we bellen opbrengst (a + b).

Als dat opbrengst functie resulteert in een blokkerende oproep, de huidige thread blokkeert deze niet. Het zal een andere code kunnen uitvoeren. Zodra de onderbroken functie zijn uitvoering heeft voltooid, kan de thread de uitvoering van het fibonacciSeq coroutine.

We kunnen onze code testen door enkele elementen uit de Fibonacci-reeks te nemen:

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

3. Het toevoegen van de Maven-afhankelijkheid voor kotlinx-coroutines

Laten we eens kijken naar de kotlinx-coroutines bibliotheek met nuttige constructies die bovenop basiscoroutines zijn gebouwd.

Laten we de afhankelijkheid toevoegen aan het kotlinx-coroutines-core bibliotheek. Merk op dat we ook het jcenter opslagplaats:

 org.jetbrains.kotlinx kotlinx-coroutines-core 0.16 centraal //jcenter.bintray.com 

4. Asynchroon programmeren met de lancering () Coroutine

De kotlinx-coroutines bibliotheek voegt veel nuttige constructies toe waarmee we asynchrone programma's kunnen maken. Laten we zeggen dat we een dure rekenfunctie hebben waaraan een Draad naar de invoerlijst:

opschorten fun dureComputation (res: MutableList) {vertraging (1000L) res.add ("word!")}

We kunnen een lancering coroutine die die suspend-functie op een niet-blokkerende manier zal uitvoeren - we moeten er een threadpool als argument aan doorgeven.

De lancering functie retourneert een Job instantie waarop we een toetreden () methode om te wachten op de resultaten:

@Test plezier gegevenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay () {// gegeven val res = mutableListOf () // bij runBlocking {val belofte = lancering (CommonPool) {dureComputation (res)} res.add ("Hallo,") belofte.join ()} / / then assertEquals (res, listOf ("Hallo,", "word!"))}

Om onze code te kunnen testen, geven we alle logica door in het runBlocking coroutine - wat een blokkerende oproep is. Daarom onze assertEquals () kan synchroon worden uitgevoerd nadat de code in de runBlocking () methode.

Merk op dat in dit voorbeeld, hoewel de lancering() methode wordt als eerste geactiveerd, het is een vertraagde berekening. De hoofdthread gaat verder door de extensie 'Hallo', String naar de resultatenlijst.

Na de vertraging van één seconde die is geïntroduceerd in de dureComputation () functie, de "woord!" Draad wordt aan het resultaat toegevoegd.

5. Coroutines zijn erg licht van gewicht

Laten we ons een situatie voorstellen waarin we 100.000 bewerkingen asynchroon willen uitvoeren. Het paaien van zo'n groot aantal threads zal erg duur zijn en zal mogelijk een OutOfMemoryException.

Gelukkig is dit bij het gebruik van de coroutines niet het geval. We kunnen zoveel blokkeerbewerkingen uitvoeren als we willen. Onder de motorkap worden die bewerkingen afgehandeld door een vast aantal threads zonder overmatige threadcreatie:

@Test plezier gegevenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory () {runBlocking {// gegeven val counter = AtomicInteger (0) val numberOfCoroutines = 100_000 // when val jobs = List (numberOfCoroutines) {counterG delay (1000) jobs.forEach {it.join ()} // dan assertEquals (counter.get (), numberOfCoroutines)}}

Merk op dat we 100.000 coroutines uitvoeren en elke run voegt een aanzienlijke vertraging toe. Desalniettemin is het niet nodig om teveel threads te maken, omdat die bewerkingen op een asynchrone manier worden uitgevoerd met behulp van thread van de Gemeenschappelijk zwembad.

6. Annulering en time-outs

Soms willen we, nadat we een langlopende asynchrone berekening hebben geactiveerd, deze annuleren omdat we niet langer geïnteresseerd zijn in het resultaat.

Wanneer we onze asynchrone actie starten met de lancering() coroutine, kunnen we de is actief vlag. Deze vlag wordt op false gezet wanneer de hoofdthread de annuleren() methode op het exemplaar van de Baan:

@Test plezier gegevenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// gegeven val job = launch (CommonPool) {while (isActive) {println ("is working")}} vertraging (1300L) // wanneer job.cancel () // dan annuleren met succes } }

Dit is een zeer elegante en gemakkelijke manier om het annuleringsmechanisme te gebruiken. Bij de asynchrone actie hoeven we alleen te controleren of het is actief vlag is gelijk aan false en onze verwerking annuleren.

Wanneer we om verwerking vragen en niet zeker weten hoeveel tijd die berekening zal kosten, is het raadzaam om de time-out voor een dergelijke actie in te stellen. Als de verwerking niet binnen de opgegeven time-out is voltooid, krijgen we een uitzondering en kunnen we er op gepaste wijze op reageren.

We kunnen de actie bijvoorbeeld opnieuw proberen:

@Test (verwacht = AnnuleringException :: klasse) plezier gegevenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {herhaal (1000) {i -> println ("Sommige dure berekening $ i ...") vertraging (500L)}}} }

Als we geen time-out definiëren, is het mogelijk dat onze thread voor altijd wordt geblokkeerd omdat die berekening vastloopt. We kunnen dat geval niet afhandelen in onze code als de time-out niet is gedefinieerd.

7. Asynchrone acties gelijktijdig uitvoeren

Laten we zeggen dat we twee asynchrone acties tegelijkertijd moeten starten en daarna op hun resultaten moeten wachten. Als onze verwerking één seconde duurt en we die verwerking twee keer moeten uitvoeren, is de looptijd van synchrone blokkering twee seconden.

Het zou beter zijn als we beide acties in afzonderlijke threads kunnen uitvoeren en op die resultaten in de hoofdthread kunnen wachten.

We kunnen gebruikmaken van de asynchroon () coroutine om dit te bereiken door de verwerking in twee afzonderlijke threads tegelijkertijd te starten:

@Test plezier gegevenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// gegeven waarde één = async (CommonPool) {someExpensiveComputation (delay)} Common delay when runBlocking {one.await () two.await ()}} // dan assertTrue (tijd <vertraging * 2)}}

Nadat we de twee dure berekeningen hebben ingediend, schorten we de coroutine op door de runBlocking () bellen. Zodra resultaten een en twee beschikbaar zijn, wordt de coroutine hervat en worden de resultaten geretourneerd. Het op deze manier uitvoeren van twee taken duurt ongeveer een seconde.

We kunnen passeren CoroutineStart.LAZY als het tweede argument voor de asynchroon () methode, maar dit betekent dat de asynchrone berekening pas wordt gestart als daarom wordt gevraagd. Omdat we om berekening vragen in het runBlocking coroutine, het betekent de oproep naar twee. wachten () wordt slechts eenmaal gemaakt een.wacht () is klaar:

@Test plezier gegevenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently () {runBlocking {val delay = 1000L val time = maatregelTimeMillis {// gegeven waarde een = async (CommonPool, CoroutineStart.LAZY) {someExpensiveZCoutine} CommonPool = als vertraging) someExpensiveComputation (delay)} // when runBlocking {one.await () two.await ()}} // dan assertTrue (tijd> vertraging * 2)}}

De luiheid van de uitvoering in dit specifieke voorbeeld zorgt ervoor dat onze code synchroon loopt. Dat gebeurt omdat wanneer we bellen wachten(), de hoofdthread is geblokkeerd en pas na een taak een voltooit taak twee wordt geactiveerd.

We moeten ons ervan bewust zijn dat asynchrone acties op een luie manier worden uitgevoerd, omdat ze op een blokkerende manier kunnen worden uitgevoerd.

8. Conclusie

In dit artikel hebben we gekeken naar de basisprincipes van Kotlin-coroutines.

Dat hebben we gezien buildSequence is de belangrijkste bouwsteen van elke coroutine. We hebben beschreven hoe de uitvoeringsstroom in deze programmeerstijl die doorgaat met doorgaan eruitziet.

Ten slotte keken we naar de kotlinx-coroutines bibliotheek die veel zeer nuttige constructies levert voor het maken van asynchrone programma's.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in het GitHub-project.