Kita sudah memakai async dan await di beberapa chapter sebelumnya — sekarang saatnya membahasnya tuntas. Bab ini membongkar pola konkurensi paralel dengan async, idiom awaitAll untuk koleksi, dan bagaimana exception dari sebuah async dirambatkan ke pemanggil. Memahami async secara mendalam penting karena builder ini adalah dasar untuk operasi concurrent fetch yang efisien di aplikasi nyata.

Sequential vs Concurrent #

Untuk memahami manfaat async, bandingkan dua pendekatan berikut:

  • Sequential: panggil suspend fun berurutan. Operasi kedua baru dimulai setelah operasi pertama selesai. Total waktu = jumlah durasi.
  • Concurrent: mulai beberapa async sekaligus, lalu await hasilnya. Operasi berjalan tumpang tindih. Total waktu ≈ durasi operasi terpanjang.
kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 1. Sequential: tidak ada konkurensi
    println("=== 1. sequential (tidak tumpang tindih) ===")
    val aSeq = ambilData("A", 100)
    val bSeq = ambilData("B", 150)
    println("[1] hasil: $aSeq + $bSeq (A selesai sebelum B dimulai)")

    // 2. async concurrent: tumpang tindih
    println()
    println("=== 2. async concurrent (tumpang tindih) ===")
    val a = async { ambilData("A", 100) }
    val b = async { ambilData("B", 150) }
    val hasilA = a.await()
    val hasilB = b.await()
    println("[2] hasil: $hasilA + $hasilB (kedua async berjalan bersamaan)")

    // 3. awaitAll: menunggu koleksi async selesai
    println()
    println("=== 3. awaitAll (koleksi async) ===")
    val daftar = (1..3).map { i -> async { ambilData("item-$i", 80) } }
    val semua = daftar.awaitAll()
    println("[3] semua hasil: $semua")

    // 4. async dan exception: ditangkap di try-catch di sekitar await
    println()
    println("=== 4. exception di async ===")
    try {
        coroutineScope {
            val gagal = async {
                delay(30)
                throw IllegalStateException("sengaja gagal")
            }
            gagal.await()
        }
    } catch (e: IllegalStateException) {
        println("[4] tangkap di scope: ${e.message}")
    }
}

suspend fun ambilData(label: String, delayMs: Long): String {
    delay(delayMs)
    return "data-$label"
}

Output yang Dihasilkan:

text
=== 1. sequential (tidak tumpang tindih) ===
[1] hasil: data-A + data-B (A selesai sebelum B dimulai)

=== 2. async concurrent (tumpang tindih) ===
[2] hasil: data-A + data-B (kedua async berjalan bersamaan)

=== 3. awaitAll (koleksi async) ===
[3] semua hasil: [data-item-1, data-item-2, data-item-3]

=== 4. exception di async ===
[4] tangkap di scope: sengaja gagal

Mari kita telaah baris-baris kode di atas:

  1. Baris 5-8 (sequential): dua pemanggilan suspend fun berurutan. Karena ambilData adalah suspend, pemanggilan kedua menunggu hingga pertama selesai. Jika kedua operasi memakan 100ms dan 150ms, total waktu sekitar 250ms.
  2. Baris 13-16 (async { ... } dua kali, lalu await): kedua async mulai berjalan saat async dipanggil — bukan saat await dipanggil. Karena keduanya berjalan paralel, total waktu ≈ 150ms (durasi ambilData("B", 150)). await() hanya menangguhkan pemanggil sampai hasil siap.
  3. Baris 21-23 ((1..3).map { async { ... } }): idiom umum untuk membuat koleksi async. Semua dimulai hampir bersamaan. .awaitAll() mengembalikan List<String> berisi hasil dalam urutan sama dengan urutan async-nya.
  4. Baris 28-37 (exception handling): blok coroutineScope di-wrap dalam try/catch. Karena exception dari async dirambat ke scope saat await dipanggil, catch di pemanggil dapat menanganinya. Tanpa coroutineScope, exception bisa bocor ke parent.

awaitAll: Konkurensi Kolektif #

awaitAll() adalah extension function pada Iterable<Deferred<T>> — menangguhkan sampai semua deferred selesai, lalu mengembalikan List<T> dengan urutan sama. Pola umum:

kotlin Pola: fan-out ke banyak service
suspend fun ambilSemua(userId: Int): ProfilLengkap = coroutineScope {
    val pengguna = async { api.pengguna(userId) }
    val foto = async { api.foto(userId) }
    val kota = async { api.kota(userId) }
    ProfilLengkap(
        pengguna = pengguna.await(),
        foto = foto.await(),
        kota = kota.await(),
    )
}

Pola di atas adalah fan-out: tiga operasi paralel yang masing-masing menghasilkan satu nilai. Karena ketiganya berjalan bersamaan, total waktu ≈ maksimum dari tiga operasi (bukan jumlah).

async dan Exception #

Exception dari async dirambatkan ke pemanggil saat await dipanggil. Tidak seperti launch (yang segera melempar ke scope), async menyimpan exception dan melemparnya hanya saat await dipanggil.

SkenarioPerilaku
async gagal, await dipanggilException dilempar dari await()
async gagal, await tidak dipanggilException dirambatkan saat scope berakhir
async di coroutineScope gagalMembatalkan saudara, lalu melempar ke parent
async di supervisorScope gagalSaudara tetap berjalan; exception tetap dirambatkan saat scope berakhir

Untuk menangkap exception dengan aman, bungkus pemanggilan dalam try/catch:

kotlin Pola: tangkap exception dari async
try {
    val hasil = coroutineScope {
        val d = async { service.berisiko() }
        d.await()
    }
    println("sukses: $hasil")
} catch (e: Exception) {
    println("gagal: ${e.message}")
}

Konkurensi Terbatas dengan Semaphore #

Untuk membatasi jumlah async yang berjalan bersamaan (mis. API yang hanya mengizinkan 5 koneksi paralel), gunakan kotlinx.coroutines.sync.Semaphore:

kotlin Pola: batasi konkurensi
val semaphore = Semaphore(5)
val hasil = items.map { item ->
    async {
        semaphore.withPermit {
            service.fetch(item)
        }
    }
}.awaitAll()

Referensi Lanjutan #

Selanjutnya #

Setelah menguasai async dan await, kita siap masuk ke komunikasi antar-coroutine. Bab Coroutine Channels akan memperkenalkan Channel<T> — stream panas (hot stream) untuk mengirim nilai antar coroutine dengan buffering dan sinkronisasi.