Structured Concurrency

CoroutineScope, hierarki Job, dan pembatalan.

Daftar isi

Structured concurrency adalah disiplin mengelola coroutine dalam sebuah hierarki, yaitu setiap coroutine diluncurkan di dalam CoroutineScope dan menjadi anak dari coroutine yang meluncurkannya. Dengan disiplin itu, kegagalan dan pembatalan menjalar searah yang pasti, dan tidak ada coroutine yang tercecer tanpa pemilik. Topik ini membahas pembentukan hierarki Job, cara coroutineScope menunggu seluruh anaknya, serta pembatalan yang menyasar induk dan berhenti pada titik suspensi.

CoroutineScope dan hierarki Job#

Setiap builder coroutine hidup di dalam sebuah scope, dan setiap coroutine yang diluncurkan mendapat objek Job. Job merepresentasikan satu pekerjaan yang punya siklus hidup, sedangkan scope menyediakan induknya. Ketika launch atau async dipanggil di dalam scope, Job yang dihasilkan menjadi anak dari Job induk, sehingga terbentuk pohon pekerjaan seperti pada diagram berikut.

Job indukJob anak AJob anak Bcancel() menjalar
Hierarki Job pada structured concurrency

Susunan pohon itu bukan sekadar gambaran, melainkan kontrak perilaku. Pembatalan pada induk menjalar ke seluruh anak, kegagalan anak menyebar ke atas, dan induk dianggap selesai hanya setelah seluruh anaknya selesai. Builder coroutineScope memperlihatkan sisi terakhir tersebut, yaitu blok yang menunggu semua anaknya selesai sebelum alur program melanjutkan baris berikutnya.

kotlin main.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    coroutineScope {
        launch {
            delay(100)
            println("Laporan bulanan selesai")
        }
        launch {
            delay(150)
            println("Statistik harian selesai")
        }
        println("Dua pekerjaan diluncurkan")
    }
    println("Pemrosesan berakhir setelah kedua pekerjaan selesai")
}
bash
# compiler Kotlin dan library coroutines yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export LIBS=/home/kal/my-project/tools/libs
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi main.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc main.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d out.jar && java -cp "out.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" MainKt
text Keluaran
Dua pekerjaan diluncurkan
Laporan bulanan selesai
Statistik harian selesai
Pemrosesan berakhir setelah kedua pekerjaan selesai

Blok coroutineScope berakhir hanya setelah laporan bulanan dan statistik harian sama-sama dicetak, meskipun tidak ada pemanggilan join sama sekali. Penungguan itu terjadi otomatis karena kedua launch adalah anak dari scope tersebut. Baris terakhir pun tidak mungkin dicetak lebih awal, sehingga tidak perlu mencatat dan menunggu setiap coroutine satu per satu untuk memastikan semuanya selesai.

Pembatalan menjalar ke seluruh anak#

Pembatalan dilakukan dengan memanggil cancel() pada Job yang ingin dihentikan. Pembatalan tidak menghentikan coroutine di tengah baris apa pun, melainkan menandainya, lalu coroutine benar-benar berhenti pada titik suspensi berikutnya karena pemanggilan suspend seperti delay melempar CancellationException. Pembatalan pada induk menjalar ke seluruh anaknya, sehingga satu panggilan cukup untuk menghentikan satu pohon pekerjaan.

kotlin batal.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val induk = launch {
        launch {
            repeat(5) { langkah ->
                println("Anak A: langkah $langkah")
                delay(100)
            }
        }
        launch {
            repeat(5) { langkah ->
                println("Anak B: langkah $langkah")
                delay(150)
            }
        }
    }
    delay(250)
    println("Pembatalan dikirim")
    induk.cancel()
    induk.join()
    println("Program berhenti setelah seluruh Job selesai")
}
bash
# kompilasi batal.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc batal.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d batal.jar && java -cp "batal.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" BatalKt
text Keluaran
Anak A: langkah 0
Anak B: langkah 0
Anak A: langkah 1
Anak B: langkah 1
Anak A: langkah 2
Pembatalan dikirim
Program berhenti setelah seluruh Job selesai

Masing-masing anak direncanakan mencetak lima langkah, tetapi keduanya berhenti di langkah 2 ketika pembatalan dikirim pada milidetik ke-250. Anak A berhenti pada delay berikutnya yang terjadwal 100 milidetik kemudian, dan anak B berhenti pada delay miliknya, sehingga tidak ada satu pun langkah 3 yang tercetak. Pemanggilan join setelah cancel memastikan seluruh proses pembatalan selesai sebelum baris terakhir dijalankan, dan pola cancel diikuti join inilah cara baku menghentikan pohon pekerjaan secara tertib.

Kepemilikan siklus hidup#

Disiplin structured concurrency menuntut setiap coroutine punya pemilik yang jelas, dan pemilik itu bertanggung jawab mengakhiri pekerjaannya. Scope dibuat untuk sebuah lingkup kerja, misalnya satu layar atau satu sesi pemrosesan, lalu dibatalkan ketika lingkup tersebut berakhir. Dengan pola itu, sumber daya seperti pool thread dan koneksi jaringan dilepas bersamaan dengan pembatalan scope-nya.

Kontrasnya adalah GlobalScope, yaitu scope global yang hidup sepanjang program. Coroutine yang diluncurkan di sana lepas dari struktur apa pun, tidak ikut dibatalkan ketika pemilik pekerjaannya selesai, dan bila mengalami kegagalan tidak ada induk yang mengetahuinya. Untuk itu GlobalScope hanya layak disebut sebagai kontras yang dihindari, sedangkan coroutine pada program nyata diluncurkan di dalam scope yang dikelola secara eksplisit.

Ringkasan#

  • Setiap coroutine diluncurkan di dalam CoroutineScope dan menjadi anak dari Job induknya, sehingga terbentuk hierarki pekerjaan yang teratur.
  • Blok coroutineScope otomatis menunggu seluruh anaknya selesai tanpa pemanggilan join satu per satu.
  • Pembatalan cancel() pada induk menjalar ke seluruh anak, dan setiap coroutine berhenti pada titik suspensi berikutnya karena delay melempar CancellationException.
  • Pola cancel diikuti join adalah cara baku menghentikan pohon pekerjaan, sedangkan GlobalScope dihindari karena coroutinesnya lepas dari struktur.

Referensi#