Pada chapter sebelumnya, kita melihat bagaimana asynchronous membuat program lebih produktif. Sekarang kita masuk ke jantung Kotlin coroutines: memahami apa sebenarnya coroutine itu, mengapa ia ringan dibanding thread, dan bagaimana ia dijadwalkan secara kooperatif. Kita akan memakai tiga konstruk dasar — runBlocking, launch, dan await — sebagai pintu masuk untuk membongkar konsep di baliknya. Setelah chapter ini, kode coroutine yang sebelumnya terlihat seperti trik otomatis akan terbaca sebagai mekanisme yang jelas.

Apa itu Coroutine #

Coroutine adalah instans komputasi yang dapat ditangguhkan (suspendable computation). Seperti thread, ia menjalankan sekumpulan perintah secara berurutan. Bedanya, coroutine dapat berhenti di tengah jalan (suspend) dan dilanjutkan kembali (resume) tanpa memblokade thread OS yang menjalankannya. Saat suspend, thread yang dipakai boleh mengerjakan coroutine lain.

Ciri utama coroutine:

CiriImplikasi praktis
RinganSatu program bisa menampung ratusan ribu coroutine aktif (vs ratusan thread OS)
KooperatifCoroutine harus secara sukarela menyerahkan kendali lewat suspend function
BerkonteksSetiap coroutine memiliki CoroutineContext (dispatcher, job, nama)
TerstrukturCoroutine anak hidup di dalam scope dan dihapus saat scope berakhir

Coroutine bukan thread. Ia lebih mirip continuation yang dijadwalkan di atas thread. Bayangkan sebuah thread sebagai ruang kerja, dan coroutine sebagai catatan pekerjaan yang bisa ditunda di tengah dan dilanjutkan kapan saja — bahkan di thread lain.

Tiga Konstruk Dasar #

Untuk memulai coroutine, kita memerlukan coroutine builder. Tiga builder yang sering dipakai pertama kali adalah runBlocking, launch, dan async. Berikut contoh kode yang memakai runBlocking dan launch:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("[main] mulai, thread=${Thread.currentThread().name}")

    // launch: fire-and-forget, mengembalikan Job
    val job: Job = launch {
        repeat(3) { i ->
            println("[coroutine] tugas-$i jalan @ ${Thread.currentThread().name}")
            delay(50)
        }
        println("[coroutine] selesai")
    }

    // await job selesai sebelum main kembali
    job.join()
    println("[main] selesai, status job=${job.isCompleted}")
}

Output yang Dihasilkan:

text
[main] mulai, thread=main
[coroutine] tugas-0 jalan @ main
[coroutine] tugas-1 jalan @ main
[coroutine] tugas-2 jalan @ main
[coroutine] selesai
[main] selesai, status job=true

Mari kita telaah baris-baris kode di atas:

  1. Baris 3 (fun main() = runBlocking { ... }): runBlocking adalah coroutine builder yang memblokade thread pemanggil sampai semua coroutine di dalam blok selesai. Biasanya dipakai di main() untuk menghubungkan dunia synchronous (entry point JVM) dengan dunia coroutine. Pemakaian di kode produksi sebaiknya dibatasi karena sifatnya yang memblokade.
  2. Baris 6 (launch { ... }): builder launch memulai coroutine baru dan langsung kembali. Coroutine ini bersifat fire-and-forget — pemanggil tidak menunggu hasil. launch mengembalikan sebuah Job yang dapat dipakai untuk memantau status, membatalkan, atau menunggu selesai.
  3. Baris 7 (repeat(3) { i -> ... }): loop sederhana yang mengeksekusi blok tiga kali. Setiap iterasi mencetak pesan dan memanggil delay(50).
  4. Baris 9 (delay(50)): fungsi delay adalah suspend function. Selama 50 milidetik, coroutine ditangguhkan dan thread main bebas mengerjakan coroutine lain. Setelah 50ms, coroutine dilanjutkan tepat di titik penangguhan.
  5. Baris 13 (job.join()): join() adalah suspend function yang menangguhkan coroutine pemanggil sampai job selesai. Tanpa join(), main bisa kembali sebelum launch selesai mencetak semua baris.

Ringan dan Kooperatif #

Mengapa coroutine dikatakan ringan? Sebuah thread OS di JVM menghabiskan ~512KB hingga 1MB memori untuk stack-nya. Sebaliknya, sebuah coroutine hanya menghabiskan beberapa ratus byte saat suspend. Itulah sebabnya kita bisa membuat 100.000 coroutine tanpa kehabisan memori — sesuatu yang tidak mungkin dengan thread OS.

Sifat kooperatif berarti coroutine tidak di-preempt oleh scheduler (berbeda dengan thread OS yang dapat di-preempt kapan saja). Sebuah coroutine tetap memegang thread sampai ia secara sukarela menangguhkan diri lewat suspend function seperti delay, await, atau fungsi suspend buatan kita. Akibatnya:

kotlin Contoh: coroutine yang tidak pernah menyerahkan kendali
runBlocking {
    launch {
        // loop tak terbatas tanpa suspend function
        while (true) {
            // tidak akan pernah memberi kesempatan ke coroutine lain
        }
    }
    // baris ini tidak akan pernah dieksekusi
}

Mekanisme di Balik Layar #

Saat kompilator Kotlin melihat sebuah suspend fun, ia menambahkan parameter Continuation<T> pada signature bytecode. Continuation ini menampung status lokal coroutine — variabel, posisi eksekusi, dan konteks — sehingga coroutine dapat di-resume di titik yang tepat. Mekanisme ini disebut Continuation Passing Style (CPS), dan kita akan mendalaminya pada chapter suspend Functions.

Diagram alur sederhana coroutine launch { delay(50); cetak() }:

  1. launch membuat objek Job baru dan menjadwalkan eksekusi.
  2. Coroutine mulai berjalan di thread main (atau dispatcher yang dipilih).
  3. Saat menemui delay(50), coroutine menyimpan status ke continuation dan menangguhkan diri.
  4. Thread main bebas; runtime menjadwalkan timer 50ms.
  5. Setelah 50ms, continuation dipanggil kembali dan coroutine melanjutkan dari baris cetak().
  6. Setelah blok launch selesai, Job.isCompleted menjadi true.

Referensi Lanjutan #

Selanjutnya #

Dengan tiga builder dasar di tangan, langkah berikutnya adalah memahami apa yang membuat coroutine dapat ditangguhkan: suspend function. Pada chapter suspend Functions, kita akan menulis fungsi suspend sendiri dan melihat bagaimana kompilator mengubahnya menjadi CPS.