Sebagian besar program modern berinteraksi dengan sumber eksternal: panggilan database, request HTTP, pembacaan file, atau komunikasi dengan sensor. Operasi semacam itu memerlukan waktu tunggu (waiting) yang bisa lebih lama daripada komputasi murni di CPU. Pemrograman asynchronous adalah cara menulis kode agar selama waktu tunggu tersebut program tetap produktif mengerjakan hal lain. Chapter ini membongkar konsep dasar pemrograman asynchronous, alasan mengapa async penting, dan pendekatan yang biasa dipakai sebelum Kotlin coroutines hadir.
Synchronous vs Asynchronous #
Kode synchronous mengeksekusi perintah satu per satu sesuai urutan penulisan. Saat sebuah fungsi memanggil operasi yang lama, pemanggil diblokade sampai operasi itu selesai. Pendekatan ini sederhana dan mudah dijejaki, tetapi boros pada saat menunggu IO: thread begitu saja menganggur padahal CPU bisa mengerjakan tugas lain.
Kode asynchronous membebaskan pemanggil dari keharusan menunggu. Ketika operasi lama dimulai, pemanggil mendaftararkan sebuah penangan kelanjutan (continuation) lalu langsung lanjut mengeksekusi perintah berikutnya. Setelah operasi lama selesai, kelanjutan dipanggil kembali dengan hasil atau galat yang terjadi.
Perbedaan praktis paling dirasakan ada pada tiga hal berikut:
| Aspek | Synchronous | Asynchronous |
|---|---|---|
| Respons UI | Terasa “macet” saat operasi panjang | UI tetap responsif karena tidak memblokade thread utama |
| Throughput IO | Satu thread hanya melayani satu operasi pada satu waktu | Satu thread bisa mengelola banyak operasi IO secara paralel |
| Skalabilitas | Bertambahnya beban berarti bertambahnya jumlah thread | Beban tinggi dapat dilayani dengan jumlah thread kecil |
Pada kode Kotlin, perbedaan ini mudah dibuktikan dengan coroutines. Kita akan pakai delay(300) sebagai pengganti operasi IO nyata yang memakan waktu ~300ms.
import kotlinx.coroutines.*
fun main() = runBlocking {
// Synchronous style: tiga panggilan berurutan, satu per satu
println("[sync] mulai")
val a = ambilData("A")
val b = ambilData("B")
val c = ambilData("C")
println("[sync] selesai, hasil=$a $b $c")
println()
// Asynchronous style: tiga panggilan paralel dengan async
println("[async] mulai")
val xa = async { ambilData("A") }
val xb = async { ambilData("B") }
val xc = async { ambilData("C") }
val hasilA = xa.await()
val hasilB = xb.await()
val hasilC = xc.await()
println("[async] selesai, hasil=$hasilA $hasilB $hasilC")
}
suspend fun ambilData(label: String): String {
println(" -> mulai $label")
delay(300)
println(" <- selesai $label")
return "data-$label"
}Output yang Dihasilkan:
[sync] mulai
-> mulai A
<- selesai A
-> mulai B
<- selesai B
-> mulai C
<- selesai C
[sync] selesai, hasil=data-A data-B data-C
[async] mulai
-> mulai A
-> mulai B
-> mulai C
<- selesai A
<- selesai B
<- selesai C
[async] selesai, hasil=data-A data-B data-CMari kita telaah baris-baris kode di atas:
- Baris 5-9 (blok sync): tiga pemanggilan
ambilData("A"),ambilData("B"),ambilData("C")dieksekusi berurutan. Pemanggilan kedua baru dimulai setelah pemanggilan pertama benar-benar selesai, begitu seterusnya. - Baris 14-16 (blok async):
async { ... }segera menjadwalkan ketiga operasi tanpa menunggu. EkspresiasyncmengembalikanDeferred<String>yang berperan sebagai future — penampung nilai yang akan tersedia nanti. - Baris 17-19 (
xa.await()dst.): pemanggilawait()menangguhkan coroutine saat ini sampai hasil tersedia. Karena ketigaasyncsudah dimulai sebelumnya, ketiganya selesai hampir bersamaan. - Baris 24 (
delay(300)): simulasi operasi IO yang memakan waktu. Fungsidelayadalah suspend function — dia melepaskan thread agar coroutine lain dapat memakai thread tersebut selama waktu tunggu.
Pada bagian sync, output menampilkan pola mulai A -> selesai A -> mulai B -> selesai B -> .... Pada bagian async, urutan berubah menjadi mulai A -> mulai B -> mulai C -> selesai A -> selesai B -> selesai C. Pola kedua inilah ciri khas asynchronous: operasi yang tumpang tindih.
Mengapa Async Penting #
Tiga keuntungan utama asynchronous: respons UI, konkurensi IO, dan throughput. Pada aplikasi desktop dan Android, thread utama (UI thread) harus tetap merespons gestur pengguna dalam hitungan milidetik; jika diblokade oleh operasi database 500ms, animasi terasa patah dan tombol terasa “nge-lag”. Pada aplikasi server, asynchronous memungkinkan satu thread ringan melayani ribuan koneksi secara bergantian tanpa overhead membuat thread OS baru (yang masing-masing menghabiskan memori ~1MB).
Pendekatan Asynchronous #
Sebelum Kotlin coroutines hadir, developer JVM memakai beberapa pendekatan dengan kekurangan masing-masing:
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
| Callbacks | Sederhana untuk kasus kecil | Callback hell: kode bersarang sulit dibaca, penanganan galat tersebar |
Future / CompletableFuture | Komposisi dengan thenApply, thenCompose | Masih verbose, return type kompleks, sulit untuk branching |
async/await (library pihak ketiga) | Kode terlihat synchronous | Biasanya memerlukan wrapper tambahan atau perubahan signature |
| Kotlin coroutines | Kode terlihat synchronous, ringan, terintegrasi bahasa | Memerlukan pemahaman structured concurrency |
Callback menempatkan kelanjutan sebagai argumen fungsi, sehingga kode bersarang dalam:
fun ambilProfil(id: String, callback: (Profil) -> Unit) {
ambilPengguna(id) { pengguna ->
ambilFoto(pengguna.idFoto) { foto ->
ambilKota(pengguna.idKota) { kota ->
callback(Profil(pengguna, foto, kota))
}
}
}
}Bayangkan setiap callback juga harus menangani galat — kedalaman indentasi bertambah cepat. Coroutines mengganti pola ini dengan kode linear:
suspend fun ambilProfil(id: String): Profil {
val pengguna = ambilPengguna(id)
val foto = ambilFoto(pengguna.idFoto)
val kota = ambilKota(pengguna.idKota)
return Profil(pengguna, foto, kota)
}Keyword suspend memberi tahu kompilator bahwa fungsi ini boleh ditangguhkan. Pemanggil seolah-olah menulis kode synchronous, tetapi di balik layar kompilator mengubah pemanggilan menjadi continuation passing style (CPS) yang akan kita bedah pada chapter suspend Functions.
Referensi Lanjutan #
- Kotlin coroutines overview — pengantar resmi konsep coroutine
- Coroutine basics
— panduan dasar builder
runBlocking,launch,async - kotlinx.coroutines core
— API reference library
kotlinx.coroutines
Selanjutnya #
Setelah memahami mengapa asynchronous diperlukan, langkah berikutnya adalah membedakan dua konsep yang sering tertukar: concurrency (menangani banyak hal sekaligus) dan parallelism (menjalankan banyak hal sekaligus) — perbedaan itu akan kita bedah pada chapter Concurrency vs Parallelism.