Pada chapter sebelumnya, kita memakai coroutineScope untuk membatasi lifetime coroutine. Konsep di baliknya disebut structured concurrency — sebuah disiplin yang membuat setiap coroutine memiliki tempat yang jelas dalam pohon induk-anak. Bab ini membongkar tiga jaminan utama structured concurrency: induk menunggu anak, exception merambat naik, dan pembatalan mengalir turun. Pemahaman ini penting agar kode coroutine tetap mudah dilacak bahkan ketika ada belasan coroutine yang berjalan bersamaan.
Hubungan Induk-Anak #
Dalam structured concurrency, setiap coroutine memiliki induk — scope tempat ia dimulai. Induk menunggu semua anak selesai sebelum dirinya sendiri dianggap selesai. Anak yang baru saja dimulai akan mewarisi konteks dari induknya (dispatcher, job, exception handler).
runBlocking (induk terluar)
└── coroutineScope (induk menengah)
├── launch (anak A)
│ └── launch (cucu A1)
└── async (anak B)Tiga jaminan pilar structured concurrency:
| Jaminan | Mekanisme |
|---|---|
| Induk menunggu anak | coroutineScope tidak kembali sampai semua anak selesai |
| Exception merambat naik | Anak yang gagal membatalkan saudara dan melempar ke induk |
| Pembatalan mengalir turun | Membatalkan induk otomatis membatalkan semua anak |
Demonstrasi Tiga Jaminan #
Kode berikut mendemonstrasikan ketiga jaminan dalam satu skenario:
import kotlinx.coroutines.*
fun main() = runBlocking {
println("[parent] mulai")
try {
coroutineScope {
// Anak 1: sukses jika sempat
launch {
try {
delay(50)
println("[anak-1] selesai")
} catch (e: CancellationException) {
println("[anak-1] dibatalkan")
}
}
// Anak 2: gagal lebih awal
launch {
delay(30)
println("[anak-2] akan melempar exception")
throw IllegalStateException("kegagalan anak-2")
}
// Anak 3: akan dibatalkan
launch {
try {
delay(200)
println("[anak-3] selesai")
} catch (e: CancellationException) {
println("[anak-3] dibatalkan")
}
}
}
} catch (e: IllegalStateException) {
println("[parent] tangkap: ${e.message}")
}
println("[parent] selesai")
}Output yang Dihasilkan:
[parent] mulai
[anak-2] akan melempar exception
[anak-1] dibatalkan
[anak-3] dibatalkan
[parent] tangkap: kegagalan anak-2
[parent] selesaiMari kita telaah baris-baris kode di atas:
- Baris 3 (
runBlocking { ... }): scope terluar.main()memblokade sampai semua coroutine di dalamnya selesai (jaranran pertama structured concurrency). - Baris 7 (
coroutineScope { ... }): scope anak. Builder ini tidak akan kembali sebelum ketigalaunchselesai — bukan hanya yang pertama keluar, tetapi semua anaknya. - Baris 9-19 (anak 1): coroutine yang akan menyelesaikan
delay(50)jika tidak dibatalkan. Bloktry/catchmenangkapCancellationExceptionuntuk mencatat bahwa ia tidak sempat selesai. - Baris 20-25 (anak 2): setelah
delay(30), anak ini melemparIllegalStateException. Karena tidak adatry/catchinternal, exception merambat ke scope. - Baris 26-36 (anak 3): coroutine paling lama (
delay(200)). Tidak akan pernah selesai secara natural — ia dibatalkan oleh scope sebelum delay-nya usai. - Baris 37-39 (
catch (e: IllegalStateException)): scope melempar ulang exception dari anak ke pemanggil.mainmenangkap dan mencetak pesan.
Output yang dihasilkan menunjukkan pola yang khas: anak-2 melempar, lalu anak-1 dan anak-3 mencatat bahwa mereka dibatalkan. Pesan selesai dari anak-1 dan anak-3 tidak pernah muncul karena keduanya dibatalkan lebih dulu.
Mengapa Ini Penting #
Tanpa structured concurrency, kode coroutine mudah bermasalah. Sebuah launch yang lupa di-join() tetap berjalan di latar — bahkan setelah pemanggil kembali. Jika launch itu melempar exception, exception tersebut bisa muncul di tempat tak terduga atau bahkan terbuang tanpa tercatat.
Structured concurrency menyelesaikan ini dengan dua cara:
- Lifetime yang jelas: setiap
launch/asyncterikat pada sebuah scope. Saat scope berakhir, semua coroutine anak dijamin selesai atau dibatalkan. - Exception yang dapat ditelusuri: anak yang gagal melempar ke induk. Induk bisa menangkap di boundary yang dipilih (biasanya di lapisan bisnis atau API publik).
Pembatalan Berantai #
Membatalkan induk otomatis membatalkan semua anak. Ini berguna ketika pengguna membatalkan operasi atau ketika timeout terjadi:
val pekerjaan = launch {
repeat(10) { i ->
launch { delay(50); println("tugas-$i") }
}
}
delay(80)
pekerjaan.cancel() // semua anak (tugas-2 dst.) dibatalkanPembatalan bersifat kooperatif — coroutine yang sedang menjalankan kode CPU-bound tanpa suspend function tidak akan bereaksi sampai ia menemui titik suspend berikutnya. Inilah mengapa delay dan await disebut cancellation points.
Referensi Lanjutan #
- Composing suspending functions
— pola
async/awaitdan structured concurrency - Kotlin coroutines overview — prinsip struktur induk-anak coroutine
Selanjutnya #
Dengan memahami structured concurrency, kita siap menelusuri apa yang sebenarnya diwariskan dari induk ke anak: CoroutineContext. Bab berikutnya akan membongkar konteks sebagai kumpulan elemen — Job, Dispatcher, exception handler — yang dapat kita susun dan manipulasi.