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).

text
runBlocking (induk terluar)
└── coroutineScope (induk menengah)
    ├── launch (anak A)
    │   └── launch (cucu A1)
    └── async (anak B)

Tiga jaminan pilar structured concurrency:

JaminanMekanisme
Induk menunggu anakcoroutineScope tidak kembali sampai semua anak selesai
Exception merambat naikAnak yang gagal membatalkan saudara dan melempar ke induk
Pembatalan mengalir turunMembatalkan induk otomatis membatalkan semua anak

Demonstrasi Tiga Jaminan #

Kode berikut mendemonstrasikan ketiga jaminan dalam satu skenario:

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

text
[parent] mulai
[anak-2] akan melempar exception
[anak-1] dibatalkan
[anak-3] dibatalkan
[parent] tangkap: kegagalan anak-2
[parent] selesai

Mari kita telaah baris-baris kode di atas:

  1. Baris 3 (runBlocking { ... }): scope terluar. main() memblokade sampai semua coroutine di dalamnya selesai (jaranran pertama structured concurrency).
  2. Baris 7 (coroutineScope { ... }): scope anak. Builder ini tidak akan kembali sebelum ketiga launch selesai — bukan hanya yang pertama keluar, tetapi semua anaknya.
  3. Baris 9-19 (anak 1): coroutine yang akan menyelesaikan delay(50) jika tidak dibatalkan. Blok try/catch menangkap CancellationException untuk mencatat bahwa ia tidak sempat selesai.
  4. Baris 20-25 (anak 2): setelah delay(30), anak ini melempar IllegalStateException. Karena tidak ada try/catch internal, exception merambat ke scope.
  5. Baris 26-36 (anak 3): coroutine paling lama (delay(200)). Tidak akan pernah selesai secara natural — ia dibatalkan oleh scope sebelum delay-nya usai.
  6. Baris 37-39 (catch (e: IllegalStateException)): scope melempar ulang exception dari anak ke pemanggil. main menangkap 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:

  1. Lifetime yang jelas: setiap launch/async terikat pada sebuah scope. Saat scope berakhir, semua coroutine anak dijamin selesai atau dibatalkan.
  2. 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:

kotlin Contoh: batalkan induk, semua anak ikut dibatalkan
val pekerjaan = launch {
    repeat(10) { i ->
        launch { delay(50); println("tugas-$i") }
    }
}
delay(80)
pekerjaan.cancel()  // semua anak (tugas-2 dst.) dibatalkan

Pembatalan 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 #

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.