Introduksjon til Kotlin Coroutines

1. Oversikt

I denne artikkelen ser vi på coroutines fra Kotlin-språket. For å si det enkelt, coroutines tillater oss å lage asynkrone programmer på en veldig flytende måte, og de er basert på begrepet Fortsett-passerer stil programmering.

Kotlin-språket gir oss grunnleggende konstruksjoner, men kan få tilgang til mer nyttige koroutiner med kotlinx-coroutines-kjerne bibliotek. Vi vil se på dette biblioteket når vi forstår de grunnleggende byggesteinene i Kotlin-språket.

2. Lage en Coroutine With BuildSequence

La oss lage en første coroutine ved hjelp av buildSequence funksjon.

Og la oss implementere en Fibonacci-sekvensgenerator ved hjelp av denne funksjonen:

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

Underskriften til en utbytte funksjonen er:

offentlig abstrakt suspendere moro avkastning (verdi: T)

De utsette nøkkelord betyr at denne funksjonen kan blokkeres. En slik funksjon kan suspendere a buildSequence coroutine.

Suspenderende funksjoner kan opprettes som standard Kotlin-funksjoner, men vi må være klar over at vi bare kan ringe dem fra en coroutine. Ellers får vi en kompilatorfeil.

Hvis vi har avbrutt samtalen i buildSequence, den samtalen vil bli transformert til den dedikerte staten i statsmaskinen. En coroutine kan sendes og tildeles en variabel som alle andre funksjoner.

I FibreSeq coroutine, har vi to suspensjonspunkter. Først når vi ringer avkastning (1) og andre når vi ringer utbytte (a + b).

Hvis det utbytte funksjonen resulterer i noen blokkeringsanrop, vil den gjeldende tråden ikke blokkere på den. Det vil være i stand til å utføre en annen kode. Når den suspenderte funksjonen er ferdig med kjøringen, kan tråden gjenoppta kjøringen av FibreSeq coroutine.

Vi kan teste koden vår ved å ta noen elementer fra Fibonacci-sekvensen:

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

3. Legge til Maven-avhengighet for kotlinx-coroutines

La oss se på kotlinx-coroutines bibliotek som har nyttige konstruksjoner som bygger på grunnleggende koroutiner.

La oss legge til avhengighet til kotlinx-coroutines-kjerne bibliotek. Merk at vi også må legge til jcenter oppbevaringssted:

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

4. Asynkron programmering ved hjelp av lansering () Coroutine

De kotlinx-coroutines biblioteket legger til mange nyttige konstruksjoner som lar oss lage asynkrone programmer. La oss si at vi har en kostbar beregningsfunksjon som legger til en String til inngangslisten:

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

Vi kan bruke en lansering coroutine som vil utføre den suspenderende funksjonen på en ikke-blokkerende måte - vi må sende en trådgruppe som et argument til den.

De lansering funksjonen returnerer a Jobb eksempel som vi kan kalle en bli med() metode for å vente på resultatene:

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

For å kunne teste koden vår, overfører vi all logikk til runBlocking coroutine - som er en blokkerende samtale. Derfor vår assertEquals () kan utføres synkront etter koden inne i runBlocking () metode.

Merk at i dette eksemplet, selv om lansere () metode utløses først, er det en forsinket beregning. Hovedtråden fortsetter ved å legge til "Hei," streng til resultatlisten.

Etter forsinkelsen på ett sekund som innføres i dyrt beregning () funksjon, "ord!" String vil bli lagt til resultatet.

5. Coroutines er veldig lette

La oss forestille oss en situasjon der vi ønsker å utføre 100000 operasjoner asynkront. Gyting av et så høyt antall tråder vil være veldig kostbart og vil muligens gi en OutOfMemoryException.

Heldigvis, når du bruker coroutines, er dette ikke et tilfelle. Vi kan utføre så mange blokkeringsoperasjoner som vi vil. Under panseret vil disse operasjonene bli håndtert av et fast antall tråder uten overdreven trådoppretting:

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

Vær oppmerksom på at vi kjører 100.000 koroutiner, og hvert løp gir en betydelig forsinkelse. Ikke desto mindre er det ikke nødvendig å lage for mange tråder fordi disse operasjonene utføres på en asynkron måte ved hjelp av tråden fra CommonPool.

6. Avbestilling og tidsavbrudd

Noen ganger, etter at vi har utløst en langvarig asynkron beregning, vil vi avbryte den fordi vi ikke lenger er interessert i resultatet.

Når vi starter vår asynkrone handling med lansere () coroutine, kan vi undersøke er aktiv flagg. Dette flagget er satt til falsk når hovedtråden påkaller Avbryt() metoden på forekomsten av Jobb:

@Test fun givenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// given val job = launch (CommonPool) {while (isActive) {println ("is working")}} delay (1300L) // når job.cancel () // deretter avbryt vellykket}}

Dette er en veldig elegant og enkel måte å bruke avbestillingsmekanismen på. I den asynkrone handlingen trenger vi bare å sjekke om er aktiv flagget er lik falsk og avbryte behandlingen.

Når vi ber om litt behandling og ikke er sikre på hvor lang tid beregningen vil ta, er det tilrådelig å sette tidsavbrudd for en slik handling. Hvis behandlingen ikke avsluttes innen gitt tidsavbrudd, får vi et unntak, og vi kan reagere på det riktig.

For eksempel kan vi prøve handlingen på nytt:

@Test (forventet = CancellationException :: class) fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {repeat (1000) {i -> println ("Some expensive calculation $ i ...") delay (500L)}}} }

Hvis vi ikke definerer et tidsavbrudd, er det mulig at tråden vår blir blokkert for alltid fordi beregningen vil henge. Vi kan ikke håndtere den saken i koden vår hvis tidsavbrudd ikke er definert.

7. Kjøre asynkrone handlinger samtidig

La oss si at vi må starte to asynkrone handlinger samtidig og vente på resultatene etterpå. Hvis behandlingen tar ett sekund, og vi må utføre behandlingen to ganger, vil kjøretiden for synkron blokkering være to sekunder.

Det ville være bedre om vi kunne kjøre begge disse handlingene i separate tråder og vente på resultatene i hovedtråden.

Vi kan utnytte asynkronisering () coroutine for å oppnå dette ved å starte behandlingen i to separate tråder samtidig:

@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 ()}} // deretter assertTrue (tid <forsinkelse * 2)}}

Etter at vi har sendt inn de to dyre beregningene, suspenderer vi coroutine ved å utføre runBlocking () anrop. En gang resultater en og to er tilgjengelig, vil coroutine gjenopptas, og resultatene returneres. Å utføre to oppgaver på denne måten bør ta rundt ett sekund.

Vi kan passere CoroutineStart.LAZY som det andre argumentet til asynkronisering () metoden, men dette vil bety at den asynkrone beregningen ikke vil bli startet før du blir bedt om det. Fordi vi ber om beregning i runBlocking coroutine, betyr det kallet til two.await () vil bli laget bare når one.await () har gjort ferdig:

@Test fun 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)}}

Latskapen med utførelsen i dette spesielle eksemplet får koden vår til å kjøre synkront. Det skjer fordi når vi ringer avvente(), er hovedtråden blokkert og bare etter oppgaven en fullfører oppgaven to vil bli utløst.

Vi må være klar over å utføre asynkrone handlinger på en lat måte, da de kan kjøre på en blokkerende måte.

8. Konklusjon

I denne artikkelen så vi på grunnleggende om Kotlin coroutines.

Vi så det buildSequence er den viktigste byggesteinen i hver coroutine. Vi beskrev hvordan gjennomføringsflyten i denne fortsettelsespasseringsprogrammeringsstilen ser ut.

Til slutt så vi på kotlinx-coroutines bibliotek som leverer mange veldig nyttige konstruksjoner for å lage asynkrone programmer.

Implementeringen av alle disse eksemplene og kodebiter finner du i GitHub-prosjektet.