Bevezetés a Kotlin Coroutines-ba
1. Áttekintés
Ebben a cikkben a korlinokat vizsgáljuk a Kotlin nyelvén. Egyszerűen fogalmazva, a coroutines lehetővé teszi számunkra, hogy aszinkron programokat hozzunk létre nagyon folyékonyan, és ezek a Folytatás-átadás stílus programozás.
A Kotlin nyelv alapvető konstrukciókat ad nekünk, de a kotlinx-coroutines-mag könyvtár. Megnézzük ezt a könyvtárat, ha megértjük a Kotlin nyelv alapvető építőköveit.
2. Koroutin létrehozása BuildSequence
Hozzunk létre egy első korutint a buildSequence funkció.
És valósítsunk meg egy Fibonacci szekvenciagenerátort a következő funkcióval:
val fibonacciSeq = buildSequence {var a = 0 var b = 1 hozam (1), míg (true) {hozam (a + b) val tmp = a + b a = b b = tmp}}
A. Aláírása hozam funkció:
nyilvános absztrakt felfüggesztés szórakoztató hozam (értéke: T)
A függessze fel kulcsszó azt jelenti, hogy ez a funkció blokkolható. Az ilyen funkció felfüggesztheti a buildSequence coroutine.
A felfüggesztő függvények létrehozhatók standard Kotlin-függvényekként, de tisztában kell lennünk azzal, hogy ezeket csak egy coroutine-ból hívhatjuk meg. Ellenkező esetben fordítói hibát kapunk.
Ha felfüggesztettük a hívást a buildSequence, ez a hívás az államgépben dedikált állapotba alakul át. A coroutine átadható és hozzárendelhető egy változóhoz, mint bármely más függvény.
Ban,-ben fibonacciSeq coroutine, két felfüggesztési pontunk van. Először, amikor telefonálunk hozam (1) és másodszor, amikor telefonálunk hozam (a + b).
Ha az hozam függvény valamilyen blokkoló hívást eredményez, az aktuális szál nem fog blokkolni rajta. Képes lesz valamilyen más kód végrehajtására. Amint a felfüggesztett függvény befejezi a végrehajtását, a szál folytathatja a fibonacciSeq coroutine.
Tesztelhetjük a kódunkat, ha a Fibonacci szekvenciából néhány elemet veszünk:
val res = fibonacciSeq .take (5) .toList () assertEquals (res, listOf (1, 1, 2, 3, 5))
3. A Maven-függőség hozzáadása ehhez: kotlinx-coroutines
Nézzük meg a kotlinx-coroutines könyvtár, amely hasznos konstrukciókkal rendelkezik az alapvető koroutinok tetején.
Adjuk hozzá a függőséget a kotlinx-coroutines-mag könyvtár. Ne feledje, hogy hozzá kell adnunk a jcenter adattár:
org.jetbrains.kotlinx kotlinx-coroutines-core 0.16 central //jcenter.bintray.com
4. Aszinkron programozás a indítás () Coroutine
A kotlinx-coroutines A könyvtár sok hasznos konstrukciót ad hozzá, amelyek segítségével aszinkron programokat hozhatunk létre. Tegyük fel, hogy van egy drága számítási függvényünk, amely a Húr a bemeneti listához:
függessze fel a szórakozást drágaComputation (res: MutableList) {delay (1000L) res.add ("word!")}
Használhatjuk a dob coroutine, amely ezt a felfüggesztési funkciót nem blokkoló módon hajtja végre - érvként át kell adnunk egy szálkészletet.
A dob függvény visszatér a Munka példány, amelyen a csatlakozik() módszer az eredmények megvárására:
@Test fun givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay () {// adott val res = mutableListOf () // when runBlocking {val ígéret = indítás (CommonPool) {drágaComputation (res)} res.add ("Hello") / ígéret.join ( / majd assertEquals (res, listOf ("Hello,", "word!"))}}
A kódunk teszteléséhez minden logikát átadunk a runBlocking coroutine - ami blokkoló hívás. Ezért a mi assertEquals () a szinkronban futtatható a kód belsejében runBlocking () módszer.
Vegye figyelembe, hogy ebben a példában, bár a dob() metódus indul el először, ez egy késleltetett számítás. A fő szál a - Hello - String az eredménylistára.
Az egy másodperc késleltetés után, amelyet a drágaComputation () funkció, a "szó!" Húr az eredményhez lesz csatolva.
5. A korutinok nagyon könnyűek
Képzeljünk el egy olyan helyzetet, amelyben 100000 műveletet akarunk aszinkron módon végrehajtani. Ilyen nagy számú szál ívása nagyon költséges lesz, és valószínűleg hozni fog OutOfMemoryException.
Szerencsére a koroutinok használatakor ez nem így van. Annyi blokkolási műveletet hajthatunk végre, amennyit csak akarunk. A motorháztető alatt ezeket a műveleteket rögzített számú szál fogja kezelni, anélkül, hogy túlzott szál létrehozása lenne:
@Test fun givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory () {runBlocking {// megadott val számláló = AtomicInteger (0) val numberOfCoroutines = 100_000 // amikor val job = List (numberOfCoroutement) {launch (CommonPlace (CommonPlace}) {StartGold (CommonPlay} {(CommonPort}) {Start} (1000) job.forEach {it.join ()} //, majd assertEquals (counter.get (), numberOfCoroutines)}}
Ne feledje, hogy 100 000 koroutint hajtunk végre, és minden egyes futtatás jelentős késéssel jár. Ennek ellenére nincs szükség túl sok szál létrehozására, mert ezeket a műveleteket aszinkron módon hajtják végre a CommonPool.
6. Lemondás és időkorlátok
Néha, miután elindítottunk néhány régóta futó aszinkron számítást, törölni szeretnénk, mert már nem érdekel bennünket az eredmény.
Amikor aszinkron akciónkat a -val kezdjük dob() coroutine, megvizsgálhatjuk a aktív zászló. Ez a jelző értéke hamis, amikor a fő szál meghívja a megszünteti() metódus a Munka:
@Test fun givenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// megadott val job = launch (CommonPool) {while (isActive) {println ("dolgozik")}} delay (1300L) // amikor job.cancel () // majd törölje sikeresen}}
Ez egy nagyon elegáns és a törlési mechanizmus egyszerű használata. Az aszinkron műveletben csak azt kell ellenőriznünk, hogy a aktív zászló egyenlő hamis és törölje a feldolgozásunkat.
Amikor némi feldolgozást kérünk, és nem vagyunk biztosak abban, hogy a számítás mennyi időbe telik, akkor célszerű beállítani az ilyen művelet időkorlátját. Ha a feldolgozás az adott időkorláton belül nem fejeződik be, kivételt kapunk, és megfelelően reagálhatunk rá.
Például megismételhetjük a műveletet:
@Test (várható = CancellationException :: class) fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {repeat (1000) {i -> println ("Néhány drága számítási $ i ...") késés (500L)}} }
Ha nem határozunk meg időkorlátot, akkor lehetséges, hogy a szálunk örökre blokkolásra kerül, mert a számítás lefagy. Nem tudjuk kezelni ezt az esetet a kódunkban, ha az időkorlát nincs meghatározva.
7. Az aszinkron műveletek egyidejű futtatása
Tegyük fel, hogy két aszinkron akciót kell egyidejűleg elindítanunk, és utána várnunk kell eredményeikre. Ha a feldolgozásunk egy másodpercet vesz igénybe, és ezt a feldolgozást kétszer kell végrehajtanunk, a szinkron blokkolás végrehajtásának futási ideje két másodperc lesz.
Jobb lenne, ha mindkét műveletet külön szálakban futtathatnánk, és megvárnánk ezeket az eredményeket a fő szálban.
Kihasználhatjuk a aszinkron () coroutine ennek elérése érdekében két külön szál egyidejű feldolgozásával:
@Test fun givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently () {runBlocking {val delay = 1000L val time = MeasureTimeMillis {// adott val one = async (CommonPool) {someExpensiveComputool as some (delay)} two runBlocking {one.await () two.await ()}} // majd assertTrue (idő <késleltetés * 2)}}
Miután elküldtük a két drága számítást, a program végrehajtásával felfüggesztjük a programot runBlocking () hívás. Miután eredményeket egy és kettő rendelkezésre állnak, a korutin folytatódik, és az eredményeket visszaküldik. Két ilyen feladat végrehajtása körülbelül egy másodpercet vesz igénybe.
Áthaladhatunk CoroutineStart.LAZY mint a második érv a aszinkron () metódus, de ez azt jelenti, hogy az aszinkron számítást csak kérésig lehet elindítani. Mivel számítást kérünk a runBlocking coroutine, ez a hívást jelenti two.await () csak egyszer készül el one.await () befejeződött:
@Test fun givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// megadott val one = async (CommonPool, CoroutineStart.LAZY) {someExpensiveCompation (néhány = ExpensiveCompation someExpensiveComputation (delay)} // runBlocking {one.await () two.await ()}}} // majd assertTrue (idő> késleltetés * 2)}}
A végrehajtás lustasága ebben a példában a kódunk szinkron futtatását okozza. Ez azért történik, mert amikor hívunk várják(), a fő szál le van tiltva, és csak a feladat után egy befejezi a feladatot kettő kiváltásra kerül.
Tisztában kell lennünk azzal, hogy az aszinkron műveleteket lustán hajtsuk végre, mivel blokkoló módon futhatnak.
8. Következtetés
Ebben a cikkben a Kotlin-korutin alapjait néztük meg.
Ezt láttuk buildSequence minden korutin fő építőeleme. Leírtuk, hogyan néz ki a végrehajtás folyamata ebben a Folytatás-átadás programozási stílusban.
Végül megnéztük a kotlinx-coroutines könyvtár, amely sok nagyon hasznos konstrukciót szállít aszinkron programok létrehozásához.
Mindezen példák és kódrészletek megvalósítása megtalálható a GitHub projektben.