Setiap coroutine memiliki CoroutineContext — kumpulan elemen yang menentukan bagaimana coroutine berjalan: dispatcher yang dipakai, job yang mengatur lifecycle, nama untuk debugging, dan handler exception. Memahami konteks penting karena banyak perilaku coroutine (thread yang dipakai, penanganan galat, naming saat trace) dikendalikan oleh elemen konteks yang aktif. Bab ini membuka kotak CoroutineContext dan menunjukkan cara menyusunnya, menggantinya, dan membacanya.
Apa itu CoroutineContext #
CoroutineContext adalah sebuah map dari key ke elemen yang terikat pada sebuah coroutine. Setiap elemen adalah objek dengan Key unik. Empat elemen yang paling sering dijumpai:
| Elemen | Key | Peran |
|---|---|---|
Job | Job | Lifecycle coroutine (active, completed, cancelled) |
CoroutineDispatcher | ContinuationInterceptor | Thread yang dipakai untuk menjalankan coroutine |
CoroutineName | CoroutineName | Nama untuk debugging dan profiling |
CoroutineExceptionHandler | CoroutineExceptionHandler | Handler exception yang tidak tertangkap |
Setiap elemen adalah CoroutineContext.Element, dan CoroutineContext sendiri dapat berupa elemen tunggal atau gabungan (CombinedContext). Kita tidak perlu mengurus implementasi internal — yang penting adalah cara mengakses dan menyusun elemen.
Mengakses Elemen #
Akses elemen lewat operator get dengan Key. Kotlin menyediakan sintaks ctx[CoroutineName] yang sebenarnya memakai companion object CoroutineName sebagai key.
import kotlinx.coroutines.*
import kotlin.coroutines.ContinuationInterceptor
fun main() = runBlocking {
// Konteks default dari runBlocking: EmptyCoroutineContext + Job
println("[default] nama=${coroutineContext[CoroutineName]}")
println("[default] job=${coroutineContext[Job]?.javaClass?.simpleName}")
// Mengganti nama dengan operator +
launch(Dispatchers.Default + CoroutineName("worker-A")) {
println("[worker-A] nama=${coroutineContext[CoroutineName]?.name}")
println("[worker-A] dispatcher=${coroutineContext[ContinuationInterceptor]}")
}.join()
// Menggabungkan beberapa elemen; tipe CombinedContext mengizinkan akses key
val ctxKustom = Dispatchers.Unconfined + CoroutineName("kustom") + CoroutineName("kustom-utama")
println()
println("[kustom] nama akhir=${ctxKustom[CoroutineName]?.name}")
println("[kustom] dispatcher=${ctxKustom[ContinuationInterceptor]}")
// Membaca elemen spesifik via index operator
println()
val job: Job? = coroutineContext[Job]
val nama: CoroutineName? = coroutineContext[CoroutineName]
println("[akses] job=${job?.javaClass?.simpleName} nama=${nama?.name}")
}Output yang Dihasilkan:
[default] nama=null
[default] job=BlockingCoroutine
[worker-A] nama=worker-A
[worker-A] dispatcher=Dispatchers.Default
[kustom] nama akhir=kustom-utama
[kustom] dispatcher=Dispatchers.Unconfined
[akses] job=BlockingCoroutine nama=nullMari kita telaah baris-baris kode di atas:
- Baris 4-7 (
runBlocking+ baca konteks default): konteksrunBlockingtidak memilikiCoroutineNamesehinggacoroutineContext[CoroutineName]mengembalikannull. AtributJobada karenarunBlockingmembuatBlockingCoroutine(turunanJob) — yang dapat kita cek tipe konkretnya. - Baris 10-13 (
launch(Dispatchers.Default + CoroutineName("worker-A"))): builderlaunchmenerimaCoroutineContextsebagai argumen pertama. EkspresiDispatchers.Default + CoroutineName("worker-A")menggabungkan dua elemen. Konteks ini menjadi konteks anak, mengganti dispatcher dan nama. - Baris 17-18 (
Dispatchers.Unconfined + CoroutineName("kustom") + CoroutineName("kustom-utama")): menggabungkan tiga elemen. KarenaCoroutineNamemuncul dua kali, elemen terakhir menang — perhatikan outputnama akhir=kustom-utama, bukankustom. Inilah cara+menangani konflik key. - Baris 22-24 (akses via index operator):
coroutineContext[Job]mengembalikanJob?. Karena variabel eksplisit bertipe, kompilator Kotlin memilih overloadgetyang tepat.job?.javaClass?.simpleNamemenghasilkan nama kelas sederhana (BlockingCoroutine) untuk identifikasi tanpa alamat memori.
Menyusun Konteks dengan Operator + #
Operator + pada CoroutineContext bukan penjumlahan biasa — ia adalah penggabungan dengan overwrite. Jika dua elemen memiliki key yang sama, elemen kedua (kanan) menang. Pola ini sering dipakai untuk mengganti sebagian konteks tanpa menyentuh yang lain.
val ctxBaru = coroutineContext + Dispatchers.IO
// ctxBaru tetap punya Job lama tetapi dispatcher diganti ke IOBeberapa aturan penyusunan:
| Ekspresi | Hasil |
|---|---|
EmptyCoroutineContext + Job | konteks dengan satu elemen |
Dispatchers.IO + Dispatchers.Default | dispatcher terakhir menang (Default) |
Job() + Job() | job terakhir menang (anak tidak mewarisi Job lama) |
konteksAnak + elemenBaru | anak memakai konteks gabungan |
Konteks yang Diwariskan ke Anak #
Saat launch atau async dipanggil di dalam sebuah scope, anak mewarisi konteks induk kecuali Job. Anak mendapat Job baru (anak dari job induk) — bukan job induk langsung. Inilah yang memungkinkan structured concurrency: membatalkan job induk membatalkan semua job anak.
Elemen lain (dispatcher, nama, exception handler) diwariskan apa adanya kecuali ditimpa secara eksplisit.
Custom CoroutineContext.Element #
Kita dapat membuat elemen konteks kustom. Kasus penggunaan umum: menyisipkan trace ID untuk logging, atau konfigurasi yang ingin diakses oleh semua coroutine anak:
class RequestId(val value: String) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<RequestId>
}
launch(RequestId("req-42")) {
val id = coroutineContext[RequestId]?.value
println("req=$id") // req=req-42
}Referensi Lanjutan #
- Coroutine context and dispatchers
— anatomi
CoroutineContext—Job,Dispatcher, name - Kotlin coroutines overview — tinjauan umum coroutine dan konteksnya
Selanjutnya #
Dengan memahami konteks, kita siap menelusuri elemen yang paling sering diubah: dispatcher. Bab Dispatchers akan membedah Dispatchers.Default, Dispatchers.IO, Dispatchers.Main, dan Dispatchers.Unconfined beserta kapan memakai masing-masing.