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:
| Status | Arti |
|---|---|
New | Dibuat tapi belum dimulai (jarang terlihat — launch langsung memulai) |
Active | Sedang berjalan |
Completing | Sedang finalisasi (transisi singkat) |
Completed | Selesai, 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.
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:
[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=trueMari kita telaah baris-baris kode di atas:
- Baris 4-18 (
launch { ... }): coroutine anak yang berjalan dalam loopwhile (isActive). Loop berhenti saatisActivemengembalikanfalseatau saatdelaymelemparCancellationException. - Baris 7 (
while (isActive)): cek kooperatif.isActiveadalah properti yang membaca status job. Setelahcancel()dipanggil,isActivemenjadifalse— loop keluar secara natural. - Baris 10 (
delay(50)): cancellation point.delaymengecek status pembatalan dan melemparCancellationExceptionjika job telah dibatalkan. Inilah titik di mana coroutine benar-benar berhenti — bukan di barisisActive, melainkan didelay. - Baris 12-14 (
catch (e: CancellationException)): menangkap pengecualian pembatalan. Penanganan di sini bersifat opsional — kita boleh log, atau bahkan mengabaikan. Yang penting: jika menangkapCancellationException, kode setelah catch akan tetap berjalan (sebaliknya, exception yang tidak ditangkap akan merambat ke scope induk). - Baris 15-17 (
finally { ... }): blokfinallyselalu dijalankan, baik loop keluar normal maupun exception. Inilah tempat yang tepat untuk menutup resource (file, koneksi, cursor). - 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 blokfinally).
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:
| Fungsi | Sifat |
|---|---|
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:
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():
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 selesaiPola 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 #
- Coroutine context and dispatchers
— lifecycle
Jobdan mekanisme pembatalan kooperatif - Coroutine basics
— dasar
Jobdari builderlaunch
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.