Setiap coroutine yang dimulai lewat launch atau async memiliki Job — objek yang merepresentasikan lifecycle dan kontrol coroutine tersebut. Dengan Job, kita bisa memeriksa status, membatalkan, atau menunggu selesai. Bab ini membongkar Job, mekanisme pembatalan kooperatif, dan jebakan umum saat menulis coroutine yang harus dapat dibatalkan dengan bersih.

Interface Job #

Job adalah interface yang merepresentasikan sebuah unit kerja dengan lifecycle. Empat status utama:

StatusArti
NewDibuat tapi belum dimulai (jarang terlihat — launch langsung memulai)
ActiveSedang berjalan
CompletingSedang finalisasi (transisi singkat)
CompletedSelesai, baik sukses maupun dibatalkan

Properti pembantu isActive, isCompleted, isCancelled memberi tahu status saat ini. Selain itu, Job menyediakan:

  • cancel(): membatalkan job (memberi tanda, tidak langsung menghentikan).
  • join(): suspend function yang menunggu sampai job selesai.
  • cancelAndJoin(): kombinasi keduanya — membatalkan dan menunggu sampai benar-benar selesai.

Pembatalan Kooperatif #

Pembatalan coroutine bersifat kooperatif: cancel() hanya memberi tanda bahwa job harus dibatalkan. Coroutine tidak otomatis berhenti — ia harus mengecek sendiri apakah sudah dibatalkan, lewat cancellation points atau pemeriksaan eksplisit.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        var iterasi = 0
        try {
            while (isActive) {
                iterasi++
                println("[job] iterasi-$iterasi jalan")
                delay(50)
            }
        } catch (e: CancellationException) {
            println("[job] dibatalkan di iterasi-$iterasi")
        } finally {
            println("[job] cleanup: menutup resource")
        }
        println("[job] setelah try-catch, ini tetap berjalan")
    }

    delay(120)
    println("[main] membatalkan job")
    job.cancelAndJoin()
    println("[main] job status: completed=${job.isCompleted} cancelled=${job.isCancelled}")
}

Output yang Dihasilkan:

text
[job] iterasi-1 jalan
[job] iterasi-2 jalan
[job] iterasi-3 jalan
[main] membatalkan job
[job] dibatalkan di iterasi-3
[job] cleanup: menutup resource
[job] setelah try-catch, ini tetap berjalan
[main] job status: completed=true cancelled=true

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-18 (launch { ... }): coroutine anak yang berjalan dalam loop while (isActive). Loop berhenti saat isActive mengembalikan false atau saat delay melempar CancellationException.
  2. Baris 7 (while (isActive)): cek kooperatif. isActive adalah properti yang membaca status job. Setelah cancel() dipanggil, isActive menjadi false — loop keluar secara natural.
  3. Baris 10 (delay(50)): cancellation point. delay mengecek status pembatalan dan melempar CancellationException jika job telah dibatalkan. Inilah titik di mana coroutine benar-benar berhenti — bukan di baris isActive, melainkan di delay.
  4. Baris 12-14 (catch (e: CancellationException)): menangkap pengecualian pembatalan. Penanganan di sini bersifat opsional — kita boleh log, atau bahkan mengabaikan. Yang penting: jika menangkap CancellationException, kode setelah catch akan tetap berjalan (sebaliknya, exception yang tidak ditangkap akan merambat ke scope induk).
  5. Baris 15-17 (finally { ... }): blok finally selalu dijalankan, baik loop keluar normal maupun exception. Inilah tempat yang tepat untuk menutup resource (file, koneksi, cursor).
  6. Baris 22 (job.cancelAndJoin()): cancel() saja tidak menunggu; join() saja tidak membatalkan. cancelAndJoin() melakukan keduanya secara atomik — membatalkan job lalu menunggu sampai benar-benar selesai (termasuk blok finally).

Output yang dihasilkan menunjukkan tiga iterasi (selama ~120ms), lalu pembatalan terjadi pada iterasi ke-3. Cleanup di finally berjalan, dan baris setelah try-catch juga tereksekusi karena exception ditangkap.

Cancellation Points #

Fungsi yang mengecek status pembatalan dan melempar CancellationException disebut cancellation point. Beberapa contoh:

FungsiSifat
delay(time)Cek saat menunda
join(), await()Cek saat menunggu
yield()Cek dan menyerahkan kendali ke coroutine lain
withTimeout(time)Membatalkan otomatis setelah waktu tertentu
channel.receive()Cek saat menunggu pesan

Jika kode Anda berjalan di CPU-bound loop tanpa cancellation point, pembatalan tidak akan diproses sampai loop keluar. Untuk loop panjang, sisipkan ensureActive() secara berkala:

kotlin Contoh: ensureActive di loop CPU-bound
suspend fun prosesBesar(data: List<Int>): Int {
    var total = 0
    for (x in data) {
        ensureActive()  // lempar CancellationException jika dibatalkan
        total += x * x
    }
    return total
}

ensureActive() adalah suspend function ringan yang hanya mengecek status. Ia melempar CancellationException jika job sudah tidak aktif, atau langsung kembali jika masih aktif.

Pembatalan Non-Kooperatif (Jebakan) #

Kode yang tidak memiliki cancellation point tidak akan merespons cancel():

kotlin Anti-pattern: loop tanpa cancellation point
val job = launch(Dispatchers.Default) {
    var i = 0
    while (i < Int.MAX_VALUE) {
        i++  // tidak ada cek pembatalan
    }
}
delay(100)
job.cancelAndJoin()  // tidak akan benar-benar berhenti sampai loop selesai

Pola ini akan terus berjalan sampai loop selesai alami. Solusinya: sisipkan ensureActive() atau yield() di dalam loop, atau pindahkan ke suspend function yang memiliki cancellation point.

Referensi Lanjutan #

Selanjutnya #

Pembatalan adalah satu sisi lifecycle coroutine; penanganan exception adalah sisi lainnya. Bab Exception Handling in Coroutines akan membahas try-catch di dalam coroutine, CoroutineExceptionHandler, dan kapan memakai SupervisorJob untuk isolasi kegagalan.