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 funberurutan. Operasi kedua baru dimulai setelah operasi pertama selesai. Total waktu = jumlah durasi. - Concurrent: mulai beberapa
asyncsekaligus, laluawaithasilnya. Operasi berjalan tumpang tindih. Total waktu ≈ durasi operasi terpanjang.
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:
=== 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 gagalMari kita telaah baris-baris kode di atas:
- Baris 5-8 (sequential): dua pemanggilan
suspend funberurutan. KarenaambilDataadalahsuspend, pemanggilan kedua menunggu hingga pertama selesai. Jika kedua operasi memakan 100ms dan 150ms, total waktu sekitar 250ms. - Baris 13-16 (
async { ... }dua kali, laluawait): keduaasyncmulai berjalan saatasyncdipanggil — bukan saatawaitdipanggil. Karena keduanya berjalan paralel, total waktu ≈ 150ms (durasiambilData("B", 150)).await()hanya menangguhkan pemanggil sampai hasil siap. - Baris 21-23 (
(1..3).map { async { ... } }): idiom umum untuk membuat koleksiasync. Semua dimulai hampir bersamaan..awaitAll()mengembalikanList<String>berisi hasil dalam urutan sama dengan urutan async-nya. - Baris 28-37 (exception handling): blok
coroutineScopedi-wrap dalamtry/catch. Karena exception dariasyncdirambat ke scope saatawaitdipanggil,catchdi pemanggil dapat menanganinya. TanpacoroutineScope, 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:
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.
| Skenario | Perilaku |
|---|---|
async gagal, await dipanggil | Exception dilempar dari await() |
async gagal, await tidak dipanggil | Exception dirambatkan saat scope berakhir |
async di coroutineScope gagal | Membatalkan saudara, lalu melempar ke parent |
async di supervisorScope gagal | Saudara tetap berjalan; exception tetap dirambatkan saat scope berakhir |
Untuk menangkap exception dengan aman, bungkus pemanggilan dalam try/catch:
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:
val semaphore = Semaphore(5)
val hasil = items.map { item ->
async {
semaphore.withPermit {
service.fetch(item)
}
}
}.awaitAll()Referensi Lanjutan #
- Composing suspending functions
— panduan resmi
asyncdanawaituntuk konkurensi terstruktur - Coroutine basics
— dasar builder
asyncyang mengembalikanDeferred
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.