Pengenalan kepada Kotlin Coroutines

1. Gambaran keseluruhan

Dalam artikel ini, kita akan melihat coroutine dari bahasa Kotlin. Ringkasnya, coroutine memungkinkan kita membuat program asinkron dengan cara yang sangat lancar , dan mereka berdasarkan konsep pengaturcaraan gaya lanjutan .

Bahasa Kotlin memberi kita konstruksi asas tetapi dapat memperoleh akses ke coroutine yang lebih berguna dengan perpustakaan inti kotlinx-coroutines . Kami akan melihat perpustakaan ini setelah memahami asas asas bahasa Kotlin.

2. Membuat Coroutine Dengan BuildSequence

Mari buat coroutine pertama menggunakan fungsi buildSequence .

Dan mari kita laksanakan penjana urutan Fibonacci menggunakan fungsi ini:

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

Tandatangan fungsi hasil adalah:

public abstract suspend fun yield(value: T)

Kata kunci tangguhkan bermaksud fungsi ini dapat menyekat. Fungsi sedemikian dapat menangguhkan coroutine buildSequence .

Fungsi menangguhkan dapat dibuat sebagai fungsi Kotlin standard, tetapi kita harus sedar bahawa kita hanya dapat memanggilnya dari dalam coroutine. Jika tidak, kami akan mendapat ralat penyusun.

Sekiranya kita telah menangguhkan panggilan dalam buildSequence, panggilan itu akan ditukar kepada keadaan khusus dalam mesin negara. Coroutine dapat disalurkan dan diberikan kepada pemboleh ubah seperti fungsi lain.

Di coroutine fibonacciSeq , kita mempunyai dua titik penggantungan. Pertama, ketika kita memanggil hasil (1) dan kedua ketika kita memanggil hasil (a + b).

Jika itu hasil keputusan majlis di beberapa sekatan panggilan, benang semasa tidak akan menyekat di atasnya. Ia akan dapat melaksanakan beberapa kod lain. Setelah fungsi yang ditangguhkan selesai pelaksanaannya, utas dapat meneruskan pelaksanaan coroutine fibonacciSeq .

Kami dapat menguji kod kami dengan mengambil beberapa elemen dari urutan Fibonacci:

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

3. Menambah Ketergantungan Maven untuk kotlinx-coroutines

Mari lihat perpustakaan kotlinx-coroutines yang mempunyai konstruk berguna yang dibina di atas coroutin asas.

Mari tambah pergantungan ke perpustakaan inti kotlinx-coroutines . Perhatikan bahawa kami juga perlu menambahkan repositori jcenter :

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

4. Pengaturcaraan Asinkron Menggunakan pelancaran () C oroutine

The kotlinx-coroutines perpustakaan menambah banyak konstruk berguna yang membolehkan kita untuk mewujudkan program-program tak segerak. Katakan bahawa kita mempunyai fungsi pengiraan yang mahal yang menambahkan String ke senarai input:

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

Kita dapat menggunakan coroutine pelancaran yang akan menjalankan fungsi suspend dengan cara yang tidak menyekat - kita perlu melewati kumpulan utas sebagai argumen kepadanya.

The pelancaran fungsi ini mengembalikan menerusi contoh di mana kita boleh memanggil menyertai () kaedah untuk menunggu keputusan:

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

Untuk dapat menguji kod kami, kami memasukkan semua logik ke coroutine runBlocking - yang merupakan panggilan menyekat. Oleh itu assertEquals kami () dapat dilaksanakan secara serentak setelah kod di dalam kaedah runBlocking () .

Perhatikan bahawa dalam contoh ini, walaupun kaedah pelancaran () dipicu terlebih dahulu, ini adalah pengiraan yang tertunda. Urutan utama akan diteruskan dengan menambahkan "Helo", String ke senarai hasil.

Selepas kelewatan satu saat yang diperkenalkan dalam fungsi mahalComputation () , "kata!" Rentetan akan ditambahkan pada hasilnya.

5. Coroutine Sangat Ringan

Mari kita bayangkan keadaan di mana kita mahu melakukan 100000 operasi secara tidak segerak. Memulakan sebilangan besar benang akan sangat mahal dan mungkin akan menghasilkan OutOfMemoryException.

Nasib baik, semasa menggunakan coroutine, ini tidak berlaku. Kita boleh menjalankan sebilangan operasi penyekat yang kita mahukan. Di bawah tudung, operasi tersebut akan dikendalikan oleh sejumlah utas tanpa membuat benang yang berlebihan:

@Test fun givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory() { runBlocking { // given val counter = AtomicInteger(0) val numberOfCoroutines = 100_000 // when val jobs = List(numberOfCoroutines) { launch(CommonPool) { delay(1000L) counter.incrementAndGet() } } jobs.forEach { it.join() } // then assertEquals(counter.get(), numberOfCoroutines) } }

Perhatikan bahawa kami melaksanakan 100,000 coroutine dan setiap larian menambah kelewatan yang besar. Walaupun begitu, tidak perlu membuat terlalu banyak utas kerana operasi tersebut dijalankan secara asinkron menggunakan utas dari CommonPool.

6. Pembatalan dan Tamat Waktu

Kadang-kadang, setelah kami mencetuskan beberapa pengiraan tak segerak yang telah lama berjalan, kami ingin membatalkannya kerana kami tidak lagi berminat dengan hasilnya.

Apabila kita memulakan tindakan tidak segerak dengan pelancaran () coroutine, kita dapat memeriksa bendera isActive . Bendera ini ditetapkan ke false setiap kali utas utama menggunakan kaedah batal () pada contoh Pekerjaan:

@Test fun givenCancellableJob_whenRequestForCancel_thenShouldQuit() { runBlocking { // given val job = launch(CommonPool) { while (isActive) { println("is working") } } delay(1300L) // when job.cancel() // then cancel successfully } }

Ini adalah kaedah yang sangat elegan dan mudah untuk menggunakan mekanisme pembatalan . Dalam tindakan tidak segerak, kita hanya perlu memeriksa sama ada bendera isActive sama dengan palsu dan membatalkan pemprosesan kita.

Apabila kami meminta beberapa pemprosesan dan tidak pasti berapa banyak masa yang akan diambil oleh pengiraan, disarankan untuk menetapkan waktu tunggu untuk tindakan tersebut. Sekiranya pemprosesan tidak selesai dalam jangka masa yang ditentukan, kami akan mendapat pengecualian, dan kami dapat bertindak balas dengan tepat.

Sebagai contoh, kita boleh mencuba semula tindakan:

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

Sekiranya kita tidak menentukan masa tamat, kemungkinan utas kita akan disekat selamanya kerana pengiraan itu akan tergantung. Kami tidak dapat menangani kes itu dalam kod kami jika masa tamat tidak ditentukan.

7. Menjalankan Tindakan Asinkron Serentak

Let's say that we need to start two asynchronous actions concurrently and wait for their results afterward. If our processing takes one second and we need to execute that processing twice, the runtime of synchronous blocking execution will be two seconds.

It would be better if we could run both those actions in separate threads and wait for those results in the main thread.

We can leverage the async() coroutine to achieve this by starting processing in two separate threads concurrently:

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

After we submit the two expensive computations, we suspend the coroutine by executing the runBlocking() call. Once results one and two are available, the coroutine will resume, and the results are returned. Executing two tasks in this way should take around one second.

We can pass CoroutineStart.LAZY as the second argument to the async() method, but this will mean the asynchronous computation will not be started until requested. Because we are requesting computation in the runBlocking coroutine, it means the call to two.await() will be made only once the one.await() has finished:

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

The laziness of the execution in this particular example causes our code to run synchronously. That happens because when we call await(), the main thread is blocked and only after task one finishes task two will be triggered.

We need to be aware of performing asynchronous actions in a lazy way as they may run in a blocking way.

8. Conclusion

In this article, we looked at basics of Kotlin coroutines.

We saw that buildSequence is the main building block of every coroutine. We described how the flow of execution in this Continuation-passing programming style looks.

Finally, we looked at the kotlinx-coroutines library that ships a lot of very useful constructs for creating asynchronous programs.

Pelaksanaan semua contoh dan potongan kod ini boleh didapati di projek GitHub.