Uvod u Kotlin Koroutine

1. Pregled

U ovom ćemo članku razmotriti podprograme iz jezika Kotlin. Jednostavno rečeno, koroutine omogućuju nam stvaranje asinkronih programa na vrlo tečan način, a temelje se na konceptu Stil za nastavak dodavanja programiranje.

Jezik Kotlin daje nam osnovne konstrukcije, ali može dobiti pristup korisnijim programima s kotlinx-koroutine-jezgra knjižnica. Ovu ćemo knjižnicu pregledati kad shvatimo osnovne gradivne elemente jezika Kotlin.

2. Stvaranje koroutine sa BuildSequence

Stvorimo prvu koprogram koristeći buildSequence funkcija.

A implementirajmo Fibonaccijev generator sljedova pomoću ove funkcije:

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

Potpis a prinos funkcija je:

javni sažetak obustavljanja zabavnog prinosa (vrijednost: T)

The obustaviti Ključna riječ znači da ova funkcija može blokirati. Takva funkcija može suspendirati a buildSequence koroutina.

Suspendirajuće funkcije mogu se stvoriti kao standardne Kotlinove funkcije, ali moramo biti svjesni da ih možemo pozivati ​​samo iz unutarnje programske potpore. U suprotnom, dobit ćemo pogrešku kompajlera.

Ako smo prekinuli poziv unutar buildSequence, taj će se poziv transformirati u posebno stanje u državnom stroju. Podprogram se može proslijediti i dodijeliti varijabli kao i svaka druga funkcija.

U fibonacciSq koroutine, imamo dvije točke ovjesa. Prvo, kad zovemo prinos (1) i drugo kad zovemo prinos (a + b).

Ako to prinos funkcija rezultira nekim blokirajućim pozivom, trenutna nit se neće blokirati na njemu. Moći će izvršiti neki drugi kod. Nakon što suspendirana funkcija završi sa svojim izvršavanjem, nit može nastaviti izvršavanje fibonacciSq koroutina.

Naš kôd možemo testirati uzimajući neke elemente iz Fibonaccijeve sekvence:

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

3. Dodavanje ovisnosti Mavena za kotlinx-koroutine

Pogledajmo kotlinx-koroutine knjižnica koja ima korisne konstrukcije nadograđene na osnovne koroutine.

Dodajmo ovisnost na kotlinx-koroutine-jezgra knjižnica. Imajte na umu da također trebamo dodati jcenter spremište:

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

4. Asinkrono programiranje pomoću pokretanje () Coroutin

The kotlinx-koroutine knjižnica dodaje puno korisnih konstrukcija koje nam omogućuju stvaranje asinkronih programa. Recimo da imamo skupu računsku funkciju koja dodaje a Niz na ulaznu listu:

obustaviti zabavu skupoCuptation (res: MutableList) {delay (1000L) res.add ("word!")}

Možemo koristiti a lansiranje podprogram koji će izvršiti tu funkciju suspendiranja na neblokirajući način - trebamo joj proslijediti spremište niti kao argument.

The lansiranje funkcija vraća a Posao instancu na kojoj možemo nazvati a pridružiti() metoda čekanja rezultata:

@Test zabava givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay () {// data val res = mutableListOf () // when runBlocking {val obećanje = pokretanje (CommonPool) {skupakomputacija (res)} res.add ("Hello,") promis.join ()} / / then assertEquals (res, listOf ("Hello,", "word!"))}

Da bismo mogli testirati naš kod, svu logiku prenosimo u runBlocking koroutina - što je blokirajući poziv. Stoga naša assertEquals () može se izvršiti sinkrono nakon koda unutar runBlocking () metoda.

Imajte na umu da je u ovom primjeru, iako pokretanje () metoda se prvo pokreće, to je odgođeno računanje. Glavna nit nastavit će se dodavanjem "Zdravo", String na popis rezultata.

Nakon jedne sekunde odgode koja je uvedena u skupo računanje () funkcija, "riječ!" Niz bit će dodan rezultatu.

5. Koroutine su vrlo lagane

Zamislimo situaciju u kojoj želimo izvršiti 100000 operacija asinkrono. Mrijest tako velikog broja niti bit će vrlo skup i vjerojatno će donijeti OutOfMemoryException.

Srećom, kada se koriste podprogrami, to nije slučaj. Možemo izvršiti onoliko operacija blokiranja koliko želimo. Ispod napa, tim će se operacijama rukovati fiksni broj niti bez pretjeranog stvaranja niti:

@Test zabava givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory () {runBlocking {// given val counter = AtomicInteger (0) val numberOfCoroutines = 100_000 // when val jobs = List (numberOfCoroutines) {pokreta (zastoj) (delay} CommonPoincreL) {Pokretanje (odgoda (Common}) jobs.forEach {it.join ()} // zatim assertEquals (counter.get (), numberOfCoroutines)}}

Imajte na umu da izvršavamo 100 000 suprograma i svako pokretanje dodaje znatno kašnjenje. Ipak, nije potrebno stvarati previše niti jer se te operacije izvršavaju na asinkroni način pomoću niti iz CommonPool.

6. Otkazivanje i vremenska ograničenja

Ponekad, nakon što smo pokrenuli neko dugotrajno asinkrono računanje, želimo ga otkazati jer nas rezultat više ne zanima.

Kada započnemo našu asinkronu akciju s pokretanje () koroutine, možemo ispitati jeActive zastava. Ova je zastava postavljena na false kad god glavna nit pozove otkazati() metoda na primjeru Posao:

@Test zabava givenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// zadati val job = pokretanje (CommonPool) {while (isActive) {println ("is working")}} delay (1300L) // when job.cancel () // then cancel uspješno}}

Ovo je vrlo elegantno i jednostavan način korištenja mehanizma otkazivanja. U asinkronoj akciji trebamo samo provjeriti je li jeActive zastava je jednaka lažno i otkažite našu obradu.

Kada zahtijevamo određenu obradu i nismo sigurni koliko vremena će računanju potrajati, preporučljivo je postaviti vremensko ograničenje za takvu radnju. Ako obrada ne završi u zadanom vremenskom ograničenju, dobit ćemo iznimku i na nju možemo odgovarajuće reagirati.

Na primjer, možemo ponoviti radnju:

@Test (očekuje se = CancellationException :: class) zabava givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {repeat (1000) {i -> println ("Neki skupi proračun $ i ...") kašnjenje (500L)}}} }

Ako ne definiramo vremensko ograničenje, moguće je da će naša nit biti zauvijek blokirana jer će taj proračun visjeti. Ne možemo se nositi s tim slučajem u našem kodu ako vremensko ograničenje nije definirano.

7. Istodobno pokretanje asinkronih radnji

Recimo da trebamo istodobno započeti dvije asinkrone radnje i nakon toga pričekati njihove rezultate. Ako naša obrada traje jednu sekundu i trebamo izvršiti tu obradu dva puta, vrijeme izvođenja sinkronog blokiranja bit će dvije sekunde.

Bilo bi bolje kad bismo obje te radnje mogli pokrenuti u zasebnim nitima i pričekati te rezultate u glavnoj niti.

Možemo iskoristiti asinkronizacija () koroutina da bi se to postiglo započinjanjem obrade u dvije zasebne niti istovremeno:

@Test zabava givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool) {someExpensiveComputation (delay)} common twoPoolComputation runBlocking {one.await () two.await ()}} // zatim assertTrue (vrijeme <kašnjenje * 2)}}

Nakon što dostavimo dva skupa izračunavanja, obustavljamo koroutinu izvršavanjem runBlocking () poziv. Jednom rezultati jedan i dva su dostupni, podprogram će se nastaviti i rezultati će se vratiti. Izvršenje dvaju zadataka na ovaj način trebalo bi trajati oko jedne sekunde.

Možemo proći CoroutineStart.LAZNO kao drugi argument asinkronizacija () metodom, ali to će značiti da asinkrono računanje neće započeti dok se ne zatraži. Budući da tražimo izračunavanje u runBlocking koroutina, to znači poziv na dva.čekaj () bit će napravljen samo jednom one.await () je završio:

@Test zabava givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool, CoroutineStart.LAZY) {someExpensiveComputation as (delay). someExpensiveComputation (delay)} // kada se pokreneBlocking {one.await () two.await ()}} // then assertTrue (time> delay * 2)}}

Lijenost izvršenja u ovom konkretnom primjeru uzrokuje da se naš kod sinkronizirano izvršava. To se događa jer kad nazovemo čekati(), glavna nit je blokirana i to samo nakon zadatka jedan završava zadatak dva će se pokrenuti.

Moramo biti svjesni izvođenja asinkronih radnji na lijen način jer se one mogu izvoditi blokirajuće.

8. Zaključak

U ovom smo članku pogledali osnove Kotlin programa.

To smo vidjeli buildSequence glavni je blok svake koroutine. Opisali smo kako izgleda tijek izvršavanja u ovom stilu programiranja koji prolazi kroz nastavak.

Napokon smo pogledali kotlinx-koroutine knjižnica koja isporučuje puno vrlo korisnih konstrukcija za stvaranje asinkronih programa.

Provedbu svih ovih primjera i isječaka koda možete pronaći u projektu GitHub.