Pengantar Kotlin Coroutines

1. Ikhtisar

Pada artikel ini, kita akan melihat coroutine dari bahasa Kotlin. Sederhananya, coroutine memungkinkan kita membuat program asynchronous dengan cara yang sangat lancar , dan mereka didasarkan pada konsep gaya pemrograman Continuation-passing .

Bahasa Kotlin memberi kita konstruksi dasar tetapi bisa mendapatkan akses ke coroutine yang lebih berguna dengan pustaka kotlinx-coroutines-core . Kami akan melihat pustaka ini setelah kami memahami blok bangunan dasar dari bahasa Kotlin.

2. Membuat Coroutine Dengan BuildSequence

Mari buat coroutine pertama menggunakan fungsi buildSequence .

Dan mari kita terapkan generator deret 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 } }

Tanda tangan dari fungsi hasil adalah:

public abstract suspend fun yield(value: T)

Kata kunci suspend berarti fungsi ini dapat memblokir. Fungsi tersebut dapat menangguhkan coroutine buildSequence .

Fungsi penangguhan dapat dibuat sebagai fungsi Kotlin standar, tetapi kita perlu menyadari bahwa kita hanya dapat memanggilnya dari dalam coroutine. Jika tidak, kita akan mendapatkan kesalahan kompiler.

Jika kita telah menangguhkan panggilan dalam buildSequence, panggilan itu akan diubah ke status khusus di mesin status. Sebuah coroutine dapat diteruskan dan ditetapkan ke variabel seperti fungsi lainnya.

Di coroutine fibonacciSeq , kami memiliki dua titik suspensi. Pertama, saat kita memanggil yield (1) dan kedua saat kita memanggil yield (a + b).

Jika fungsi hasil itu menghasilkan beberapa panggilan pemblokiran, thread saat ini tidak akan memblokirnya. Ini akan dapat menjalankan beberapa kode lain. Setelah fungsi yang ditangguhkan menyelesaikan eksekusinya, utas dapat melanjutkan eksekusi coroutine fibonacci .

Kita dapat menguji kode kita dengan mengambil beberapa elemen dari deret Fibonacci:

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

3. Menambahkan Dependensi Maven untuk kotlinx-coroutines

Mari kita lihat pustaka kotlinx-coroutines yang memiliki konstruksi berguna yang dibangun di atas coroutine dasar.

Mari tambahkan ketergantungan ke pustaka kotlinx-coroutines-core . Perhatikan bahwa kita juga perlu menambahkan repositori jcenter :

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

4. Pemrograman Asynchronous Menggunakan launch () C oroutine

The kotlinx-coroutines perpustakaan menambahkan banyak konstruksi berguna yang memungkinkan kita untuk membuat program asynchronous. Katakanlah kita memiliki fungsi komputasi mahal yang menambahkan String ke daftar input:

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

Kita bisa menggunakan coroutine peluncuran yang akan menjalankan fungsi penangguhan dengan cara non-pemblokiran - kita perlu meneruskan kumpulan utas sebagai argumen untuk itu.

Fungsi peluncuran mengembalikan instance Job di mana kita bisa memanggil metode join () untuk menunggu hasilnya:

@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 kode kami, kami meneruskan semua logika ke dalam runBlocking coroutine - yang merupakan panggilan pemblokiran. Oleh karena itu assertEquals () kita dapat dieksekusi secara sinkron setelah kode di dalam metode runBlocking () .

Perhatikan bahwa dalam contoh ini, meskipun metode launch () dipicu terlebih dahulu, ini adalah komputasi yang tertunda. Utas utama akan melanjutkan dengan menambahkan String “Halo,” ke daftar hasil.

Setelah penundaan satu detik yang diperkenalkan dalam fungsi mahalComputation () , "kata!" String akan ditambahkan ke hasil.

5. Coroutine Sangat Ringan

Mari kita bayangkan situasi di mana kita ingin melakukan 100000 operasi secara asinkron. Memunculkan sejumlah besar thread akan sangat mahal dan mungkin akan menghasilkan OutOfMemoryException.

Untungnya, saat menggunakan coroutine, hal ini tidak terjadi. Kami dapat menjalankan operasi pemblokiran sebanyak yang kami inginkan. Di bawah kap, operasi tersebut akan ditangani oleh sejumlah utas tetap tanpa pembuatan utas 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 bahwa kami menjalankan 100.000 coroutine dan setiap proses menambahkan penundaan yang cukup besar. Namun demikian, tidak perlu membuat terlalu banyak utas karena operasi tersebut dijalankan secara asinkron menggunakan utas dari CommonPool.

6. Pembatalan dan Batas Waktu

Terkadang, setelah kami memicu beberapa komputasi asinkron yang berjalan lama, kami ingin membatalkannya karena kami tidak lagi tertarik dengan hasilnya.

Saat kita memulai tindakan asinkron kita dengan launch () coroutine, kita bisa memeriksa flag isActive . Bendera ini disetel ke false setiap kali utas utama memanggil metode cancel () pada instance Job:

@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 cara yang sangat elegan dan mudah untuk menggunakan mekanisme pembatalan . Dalam tindakan asynchronous, kita hanya perlu memeriksa apakah flag isActive sama dengan false dan membatalkan pemrosesan kita.

Saat kami meminta beberapa pemrosesan dan tidak yakin berapa banyak waktu yang dibutuhkan untuk penghitungan tersebut, sebaiknya setel batas waktu untuk tindakan seperti itu. Jika pemrosesan tidak selesai dalam waktu tunggu tertentu, kami akan mendapatkan pengecualian, dan kami dapat menanggapinya dengan tepat.

Misalnya, kita dapat mencoba kembali tindakan:

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

Jika kita tidak menentukan batas waktu, ada kemungkinan utas kita akan diblokir selamanya karena penghitungan itu akan hang. Kami tidak dapat menangani kasus itu dalam kode kami jika batas waktu tidak ditentukan.

7. Menjalankan Tindakan Asinkron Secara Bersamaan

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.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di proyek GitHub.