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:

ElemenKeyPeran
JobJobLifecycle coroutine (active, completed, cancelled)
CoroutineDispatcherContinuationInterceptorThread yang dipakai untuk menjalankan coroutine
CoroutineNameCoroutineNameNama untuk debugging dan profiling
CoroutineExceptionHandlerCoroutineExceptionHandlerHandler 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.

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

text
[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=null

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-7 (runBlocking + baca konteks default): konteks runBlocking tidak memiliki CoroutineName sehingga coroutineContext[CoroutineName] mengembalikan null. Atribut Job ada karena runBlocking membuat BlockingCoroutine (turunan Job) — yang dapat kita cek tipe konkretnya.
  2. Baris 10-13 (launch(Dispatchers.Default + CoroutineName("worker-A"))): builder launch menerima CoroutineContext sebagai argumen pertama. Ekspresi Dispatchers.Default + CoroutineName("worker-A") menggabungkan dua elemen. Konteks ini menjadi konteks anak, mengganti dispatcher dan nama.
  3. Baris 17-18 (Dispatchers.Unconfined + CoroutineName("kustom") + CoroutineName("kustom-utama")): menggabungkan tiga elemen. Karena CoroutineName muncul dua kali, elemen terakhir menang — perhatikan output nama akhir=kustom-utama, bukan kustom. Inilah cara + menangani konflik key.
  4. Baris 22-24 (akses via index operator): coroutineContext[Job] mengembalikan Job?. Karena variabel eksplisit bertipe, kompilator Kotlin memilih overload get yang tepat. job?.javaClass?.simpleName menghasilkan 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.

kotlin Contoh: ganti dispatcher, pertahankan job
val ctxBaru = coroutineContext + Dispatchers.IO
// ctxBaru tetap punya Job lama tetapi dispatcher diganti ke IO

Beberapa aturan penyusunan:

EkspresiHasil
EmptyCoroutineContext + Jobkonteks dengan satu elemen
Dispatchers.IO + Dispatchers.Defaultdispatcher terakhir menang (Default)
Job() + Job()job terakhir menang (anak tidak mewarisi Job lama)
konteksAnak + elemenBaruanak 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:

kotlin Contoh: custom element untuk request ID
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 #

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.