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:
| Ciri | Implikasi praktis |
|---|---|
| Ringan | Satu program bisa menampung ratusan ribu coroutine aktif (vs ratusan thread OS) |
| Kooperatif | Coroutine harus secara sukarela menyerahkan kendali lewat suspend function |
| Berkonteks | Setiap coroutine memiliki CoroutineContext (dispatcher, job, nama) |
| Terstruktur | Coroutine 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:
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:
[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=trueMari kita telaah baris-baris kode di atas:
- Baris 3 (
fun main() = runBlocking { ... }):runBlockingadalah coroutine builder yang memblokade thread pemanggil sampai semua coroutine di dalam blok selesai. Biasanya dipakai dimain()untuk menghubungkan dunia synchronous (entry point JVM) dengan dunia coroutine. Pemakaian di kode produksi sebaiknya dibatasi karena sifatnya yang memblokade. - Baris 6 (
launch { ... }): builderlaunchmemulai coroutine baru dan langsung kembali. Coroutine ini bersifat fire-and-forget — pemanggil tidak menunggu hasil.launchmengembalikan sebuahJobyang dapat dipakai untuk memantau status, membatalkan, atau menunggu selesai. - Baris 7 (
repeat(3) { i -> ... }): loop sederhana yang mengeksekusi blok tiga kali. Setiap iterasi mencetak pesan dan memanggildelay(50). - Baris 9 (
delay(50)): fungsidelayadalah suspend function. Selama 50 milidetik, coroutine ditangguhkan dan threadmainbebas mengerjakan coroutine lain. Setelah 50ms, coroutine dilanjutkan tepat di titik penangguhan. - Baris 13 (
job.join()):join()adalah suspend function yang menangguhkan coroutine pemanggil sampaijobselesai. Tanpajoin(),mainbisa kembali sebelumlaunchselesai 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:
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() }:
launchmembuat objekJobbaru dan menjadwalkan eksekusi.- Coroutine mulai berjalan di thread
main(atau dispatcher yang dipilih). - Saat menemui
delay(50), coroutine menyimpan status ke continuation dan menangguhkan diri. - Thread
mainbebas; runtime menjadwalkan timer 50ms. - Setelah 50ms, continuation dipanggil kembali dan coroutine melanjutkan dari baris
cetak(). - Setelah blok
launchselesai,Job.isCompletedmenjaditrue.
Referensi Lanjutan #
- Coroutine basics
— panduan resmi
runBlocking,launch,Job - Kotlin coroutines overview — konsep ringan dan penjadwalan kooperatif
- kotlinx.coroutines core
— API reference library
kotlinx.coroutines
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.