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:

AspekSynchronousAsynchronous
Respons UITerasa “macet” saat operasi panjangUI tetap responsif karena tidak memblokade thread utama
Throughput IOSatu thread hanya melayani satu operasi pada satu waktuSatu thread bisa mengelola banyak operasi IO secara paralel
SkalabilitasBertambahnya beban berarti bertambahnya jumlah threadBeban 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.

kotlin
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:

text
[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-C

Mari kita telaah baris-baris kode di atas:

  1. 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.
  2. Baris 14-16 (blok async): async { ... } segera menjadwalkan ketiga operasi tanpa menunggu. Ekspresi async mengembalikan Deferred<String> yang berperan sebagai future — penampung nilai yang akan tersedia nanti.
  3. Baris 17-19 (xa.await() dst.): pemanggil await() menangguhkan coroutine saat ini sampai hasil tersedia. Karena ketiga async sudah dimulai sebelumnya, ketiganya selesai hampir bersamaan.
  4. Baris 24 (delay(300)): simulasi operasi IO yang memakan waktu. Fungsi delay adalah 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:

PendekatanKelebihanKekurangan
CallbacksSederhana untuk kasus kecilCallback hell: kode bersarang sulit dibaca, penanganan galat tersebar
Future / CompletableFutureKomposisi dengan thenApply, thenComposeMasih verbose, return type kompleks, sulit untuk branching
async/await (library pihak ketiga)Kode terlihat synchronousBiasanya memerlukan wrapper tambahan atau perubahan signature
Kotlin coroutinesKode terlihat synchronous, ringan, terintegrasi bahasaMemerlukan pemahaman structured concurrency

Callback menempatkan kelanjutan sebagai argumen fungsi, sehingga kode bersarang dalam:

kotlin Contoh: callback bersarang (ilustratif)
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:

kotlin Versi suspend function
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 #

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.