Builder Coroutine

launch, async dengan await, dan batasan runBlocking.

Daftar isi

Builder adalah fungsi yang membuat dan memulai coroutine dalam satu panggilan. Kotlin menyediakan tiga builder yang digunakan dalam kebanyakan program, yaitu runBlocking, launch, dan async. Topik ini membahas peran masing-masing builder, cara launch bekerja tanpa mengirim hasil, cara async mengirim hasil melalui await, serta batasan runBlocking yang membatasi pemakaiannya.

runBlocking sebagai jembatan#

Builder runBlocking menjalankan blok coroutine sambil memblokir thread pemanggilnya sampai seluruh isi blok selesai. Sifat pemblokiran itu menjadikannya jembatan antara dunia biasa yang tidak mengenal suspend dan dunia coroutine yang mengenalnya, misalnya pada fungsi main, program terminal, atau test. Semua contoh pada modul ini menggunakan pola fun main() = runBlocking { ... } karena pola tersebut menghubungkan titik masuk program dengan kode coroutine.

Di dalam bloknya, runBlocking berperilaku seperti scope biasa, yaitu coroutine di dalamnya dapat diluncurkan, di-join, dan dibatalkan. Blok tersebut punya nilai kembalian, yaitu hasil ekspresi terakhir di dalamnya, sehingga runBlocking juga dapat dipakai sebagai pengirim nilai dari dunia coroutine ke dunia biasa. Perbedaannya dengan builder lain terletak pada cara blok itu ditunggu, yaitu thread pemanggil benar-benar berhenti sampai seluruh pekerjaan selesai, termasuk seluruh coroutine anak yang diluncurkan di dalamnya. Karena itu runBlocking cocok untuk program kecil dan pengujian, tetapi tidak untuk kode produksi yang seharusnya menjaga thread tetap hidup.

launch untuk kerja tanpa hasil#

Builder launch memulai coroutine baru yang bekerja tanpa mengirim hasil ke pemanggilnya, pola yang lazim disebut fire-and-forget. Fungsi tersebut segera mengembalikan objek Job, yaitu pegangan untuk menunggu, membatalkan, atau memeriksa status coroutine yang baru diluncurkan. Nama polanya perlu dibaca dengan hati-hati, karena lupa terhadap pekerjaannya bukan berarti hilang, melainkan pekerjaan tetap berjalan pada coroutine sendiri, dan hasil akhirnya, misalnya pesan yang dicetak, tidak dikirim kembali ke pemanggil. Program berikut meluncurkan satu coroutine, mencetak sesuatu setelahnya, lalu menunggu coroutine itu dengan join.

kotlin main.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val pekerjaan = launch {
        delay(100)
        println("Laporan selesai dibuat")
    }
    println("Coroutine diluncurkan, program melanjutkan eksekusi")
    pekerjaan.join()
    println("Program berhenti setelah coroutine selesai")
}
bash
# compiler Kotlin dan library coroutines yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export LIBS=/home/kal/my-project/tools/libs
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi main.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc main.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d out.jar && java -cp "out.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" MainKt
text Keluaran
Coroutine diluncurkan, program melanjutkan eksekusi
Laporan selesai dibuat
Program berhenti setelah coroutine selesai

Urutan keluarannya memperlihatkan sifat launch yang tidak menunggu. Baris pertama dicetak segera setelah launch dipanggil, jauh sebelum pekerjaan di dalamnya selesai, karena launch hanya memulai coroutine lalu langsung kembali membawa Job. Pemanggilan join kemudian menghentikan sementara coroutine pemanggil sampai pekerjaan itu benar-benar selesai, sehingga baris terakhir tidak pernah dicetak sebelum laporannya siap. Tanpa join, urutan kedua baris tidak dapat dipastikan, karena tidak ada jaminan berapa cepat coroutine yang baru diluncurkan sampai ke pencetakannya.

async dan await untuk hasil#

Builder async digunakan ketika coroutine perlu mengirim nilai kembali. Bentuk kembaliannya adalah Deferred<T>, yaitu Job yang juga membawa hasil yang akan datang, dan nilai itu diambil dengan pemanggilan await yang menunggu sampai hasilnya siap. Beberapa async dapat diluncurkan sekaligus untuk pekerjaan yang saling tidak bergantung, lalu seluruh hasilnya dijumlahkan setelahnya.

Urutan penulisan menentukan apakah pekerjaan berjalan bersamaan atau berurutan. Seluruh async perlu diluncurkan lebih dahulu, baru seluruh await dipanggil, sebagaimana program berikut. Apabila await panggil terlalu dini, misalnya async { ... }.await() langsung pada satu baris, coroutine berikutnya baru dimulai setelah yang sebelumnya selesai, sehingga kemampuan berjalan bersamanya hilang tanpa ada pesan error apa pun.

kotlin jumlah.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val pertama = async {
        delay(100)
        2
    }
    val kedua = async {
        delay(150)
        3
    }
    val jumlah = pertama.await() + kedua.await()
    println("Jumlah: $jumlah")
}
bash
# kompilasi jumlah.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc jumlah.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d jumlah.jar && java -cp "jumlah.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" JumlahKt
text Keluaran
Jumlah: 5

Kedua coroutine mulai bekerja hampir bersamaan karena masing-masing diluncurkan dengan async tanpa menunggu yang lain. Pemanggilan pertama.await() menunggu nilai 2 dan kedua.await() menunggu nilai 3, lalu keduanya dijumlahkan menjadi 5. Apabila kedua pekerjaan dihitung secara berurutan dengan pemanggilan suspend biasa, total waktunya menjadi penjumlahan kedua durasinya, sedangkan dengan async waktu tunggunya bertumpuk pada satu jeda terpanjang saja.

Batasan runBlocking#

runBlocking membayar kenyamanannya dengan thread yang diblokir penuh. Pada aplikasi nyata, misalnya server yang melayani banyak permintaan, memblokir satu thread untuk sekadar menjembatani main berarti membuang peluang menjalankan coroutine lain di thread tersebut. Karena itu pemakaiannya lazim dibatasi pada titik masuk program, program terminal kecil, dan pengujian, sedangkan bagian dalam aplikasi mengelola coroutine lewat CoroutineScope sendiri yang dibahas pada bagian structured concurrency.

Ada pula perangkap yang lebih serius, yaitu memanggil runBlocking dari dalam coroutine yang berjalan pada pool thread yang sama. Thread yang diblokir oleh runBlocking justru dibutuhkan coroutine di dalamnya untuk menyelesaikan pekerjaan, sehingga keduanya saling menunggu dan program macet permanen. Gejala semacam ini disebut deadlock, dan pencegahannya sederhana, yaitu tidak pernah menyusun runBlocking di dalam kode coroutine lain.

Ringkasan#

  • Builder membuat dan memulai coroutine sekaligus, dengan tiga bentuk utama yaitu runBlocking, launch, dan async.
  • runBlocking memblokir thread pemanggil sampai seluruh blok selesai sehingga cocok sebagai jembatan pada main dan pengujian, tetapi bukan untuk kode produksi.
  • launch berpola fire-and-forget dan mengembalikan Job, sedangkan async mengembalikan Deferred<T> yang nilainya diambil dengan await.
  • runBlocking yang dipanggil dari dalam coroutine pada pool yang sama berisiko deadlock, sehingga pemakaiannya dibatasi pada titik masuk program dan pengujian.

Referensi#