Introduktion til Kotlin Coroutines

1. Oversigt

I denne artikel ser vi på coroutines fra Kotlin-sproget. Kort fortalt, coroutines giver os mulighed for at oprette asynkrone programmer på en meget flydende måde, og de er baseret på begrebet Fortsættelse-passerer stil programmering.

Kotlin-sproget giver os grundlæggende konstruktioner, men kan få adgang til mere nyttige coroutines med kotlinx-coroutines-kerne bibliotek. Vi kigger på dette bibliotek, når vi først forstår de grundlæggende byggesten i Kotlin-sproget.

2. Oprettelse af en Coroutine With BuildSequence

Lad os oprette en første coroutine ved hjælp af buildSequence fungere.

Og lad os implementere en Fibonacci-sekvensgenerator ved hjælp af denne funktion:

val FibstaSeq = buildSequence {var a = 0 var b = 1 udbytte (1) mens (true) {udbytte (a + b) val tmp = a + b a = b b = tmp}}

Underskriften af ​​en udbytte funktion er:

offentlig abstrakt suspendere sjovt udbytte (værdi: T)

Det suspendere nøgleord betyder, at denne funktion kan blokere. En sådan funktion kan suspendere a buildSequence coroutine.

Suspensionsfunktioner kan oprettes som standard Kotlin-funktioner, men vi skal være opmærksomme på, at vi kun kan kalde dem fra en coroutine. Ellers får vi en compiler-fejl.

Hvis vi har suspenderet opkald inden for buildSequence, dette opkald vil blive omdannet til den dedikerede tilstand i statsmaskinen. En coroutine kan sendes og tildeles en variabel som enhver anden funktion.

I FibreSeq coroutine, vi har to suspensionspunkter. Først når vi ringer udbytte (1) og for det andet, når vi ringer udbytte (a + b).

Hvis det udbytte funktion resulterer i et blokerende opkald, den aktuelle tråd blokerer ikke på det. Det vil være i stand til at udføre en anden kode. Når den suspenderede funktion er færdig med udførelsen, kan tråden genoptage udførelsen af FibreSeq coroutine.

Vi kan teste vores kode ved at tage nogle elementer fra Fibonacci-sekvensen:

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

3. Tilføjelse af Maven-afhængighed for kotlinx-coroutines

Lad os se på kotlinx-coroutines bibliotek, der har nyttige konstruktioner, der bygger oven på grundlæggende coroutines.

Lad os tilføje afhængighed af kotlinx-coroutines-kerne bibliotek. Bemærk, at vi også skal tilføje jcenter lager:

 org.jetbrains.kotlinx kotlinx-coroutines-core 0,16 central //jcenter.bintray.com 

4. Asynkron programmering ved hjælp af lancering () Coroutine

Det kotlinx-coroutines bibliotek tilføjer en masse nyttige konstruktioner, der giver os mulighed for at oprette asynkrone programmer. Lad os sige, at vi har en dyr beregningsfunktion, der tilføjer en Snor til inputlisten:

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

Vi kan bruge en lancering coroutine, der udfører den suspenderende funktion på en ikke-blokerende måde - vi skal sende en trådpulje som et argument til den.

Det lancering funktionen vender tilbage a Job eksempel, som vi kan kalde en tilslutte() metode til at vente på resultaterne:

@Test sjov givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay () {// given val res = mutableListOf () // when runBlocking {val lover = launch (CommonPool) {expensiveComputation (res)} res.add ("Hello,") promise.join ()} / / derefter assertEquals (res, listOf ("Hello", "word!"))}

For at kunne teste vores kode videregiver vi al logik til runBlocking coroutine - som er et blokerende opkald. Derfor vores assertEquals () kan udføres synkront efter koden inde i runBlocking () metode.

Bemærk, at i dette eksempel, selvom lancering () metode udløses først, er det en forsinket beregning. Hovedtråden fortsætter ved at tilføje "Hej," streng til resultatlisten.

Efter forsinkelsen på et sekund, der indføres i dyrt beregning () funktion, den "ord!" Snor tilføjes til resultatet.

5. Coroutines er meget lette

Lad os forestille os en situation, hvor vi ønsker at udføre 100000 operationer asynkront. Gydning af et så stort antal tråde vil være meget dyrt og muligvis give en OutOfMemoryException.

Heldigvis er dette ikke tilfældet, når du bruger coroutines. Vi kan udføre så mange blokeringsoperationer, som vi vil. Under emhætten håndteres disse operationer af et fast antal tråde uden overdreven oprettelse af tråde:

@Test-sjov givetHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory () {runBlocking {// given val counter = AtomicInteger (0) val numberOfCoroutines = 100_000 // when val jobs = List (numberOfCoroutines) {forsinkelse (Common )ool) jobs.forEach {it.join ()} // derefter assertEquals (counter.get (), numberOfCoroutines)}}

Bemærk, at vi udfører 100.000 koroutiner, og hver kørsel tilføjer en betydelig forsinkelse. Ikke desto mindre er der ikke behov for at oprette for mange tråde, fordi disse operationer udføres på en asynkron måde ved hjælp af tråd fra CommonPool.

6. Annullering og timeouts

Nogle gange, efter at vi har udløst en langvarig asynkron beregning, vil vi annullere den, fordi vi ikke længere er interesserede i resultatet.

Når vi starter vores asynkrone handling med lancering () coroutine, kan vi undersøge er aktiv flag. Dette flag er indstillet til falsk, når hovedtråden påberåber sig afbestille() metode i tilfælde af Job:

@Test sjov givenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// given val job = launch (CommonPool) {while (isActive) {println ("is working")}} delay (1300L) // når job.cancel () // derefter annullere succesfuldt } }

Dette er en meget elegant og nem måde at bruge annulleringsmekanismen på. I den asynkrone handling skal vi kun kontrollere, om er aktiv flag er lig med falsk og annullere vores behandling.

Når vi anmoder om en vis behandling og ikke er sikre på, hvor lang tid beregningen tager, anbefales det at indstille timeout for en sådan handling. Hvis behandlingen ikke afsluttes inden for den givne timeout, får vi en undtagelse, og vi kan reagere korrekt på den.

For eksempel kan vi prøve handlingen igen:

@Test (forventet = CancellationException :: klasse) sjov givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {gentagelse (1000) {i -> println ("Nogle dyre beregninger $ i ...") forsinkelse (500L)}}} }

Hvis vi ikke definerer en timeout, er det muligt, at vores tråd bliver blokeret for evigt, fordi den beregning hænger. Vi kan ikke håndtere denne sag i vores kode, hvis timeout ikke er defineret.

7. Kørsel af asynkrone handlinger samtidigt

Lad os sige, at vi er nødt til at starte to asynkrone handlinger samtidigt og vente på deres resultater bagefter. Hvis vores behandling tager et sekund, og vi har brug for at udføre behandlingen to gange, vil kørselstiden for synkron blokering udføres to sekunder.

Det ville være bedre, hvis vi kunne køre begge disse handlinger i separate tråde og vente på disse resultater i hovedtråden.

Vi kan udnytte asynkronisering () coroutine for at opnå dette ved at starte behandling i to separate tråde samtidigt:

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

Når vi har indsendt de to dyre beregninger, suspenderer vi coroutinen ved at udføre runBlocking () opkald. Én gang resultater en og to er tilgængelige, vil coroutinen genoptages, og resultaterne returneres. At udføre to opgaver på denne måde tager cirka et sekund.

Vi kan passere CoroutineStart.LAZY som det andet argument til asynkronisering () metode, men dette vil betyde, at den asynkrone beregning ikke startes, før den bliver anmodet om. Fordi vi anmoder om beregning i runBlocking coroutine, betyder det kaldet til two.await () vil kun blive foretaget en gang one.await () er færdig:

@Test-sjov givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool, CoroutineStart.LAZY) {someExpensiveComputation (delay)} common someExpensiveComputation (delay)} // når runBlocking {one.await () two.await ()}} // derefter assertTrue (time> delay * 2)}}

Den dovne udførelse i dette særlige eksempel får vores kode til at køre synkront. Det sker, fordi når vi ringer vente(), er hovedtråden blokeret og kun efter opgave en afslutter opgaven to udløses.

Vi skal være opmærksomme på at udføre asynkrone handlinger på en doven måde, da de kan køre blokerende.

8. Konklusion

I denne artikel kiggede vi på det grundlæggende i Kotlin coroutines.

Vi så det buildSequence er den vigtigste byggesten i hver coroutine. Vi beskrev, hvordan udførelsesstrømmen i denne fortsættelsespasserende programmeringsstil ser ud.

Endelig kiggede vi på kotlinx-coroutines bibliotek, der sendes en masse meget nyttige konstruktioner til oprettelse af asynkrone programmer.

Implementeringen af ​​alle disse eksempler og kodestykker findes i GitHub-projektet.