Coroutine dapat berjalan di thread mana pun — yang menentukan adalah dispatcher yang dipakai. Salah memilih dispatcher berakibat pada kode yang lambat (operasi IO memblokade thread CPU), responsifitas UI yang drop, atau exception tak terduga di lingkungan terbatas seperti Android Main. Bab ini membedah empat dispatcher bawaan, perbedaan thread pool mereka, dan pola withContext untuk berpindah dispatcher sementara. Setelah chapter ini, Anda akan tahu persis kapan memakai Dispatchers.IO versus Dispatchers.Default.

Empat Dispatcher Bawaan #

kotlinx.coroutines menyediakan empat dispatcher utama:

DispatcherThread PoolCocok untukHindari untuk
Dispatchers.DefaultShared dengan IO, batas = core CPUKomputasi CPU-bound (sort, parse, transform)Operasi blocking seperti Thread.sleep
Dispatchers.IOShared dengan Default, batas 64+IO blocking (file, jaringan, database JDBC)Komputasi berat yang akan kelaparan thread
Dispatchers.MainThread UI tunggal (platform-specific)Update UI, interaksi penggunaKomputasi berat atau IO blocking
Dispatchers.UnconfinedTidak ada (ikuti pemanggil)Hanya untuk fungsi interop; demonstrasiKode produksi umum

Dispatchers.Default dan Dispatchers.IO sebenarnya berbagi thread pool yang sama (diimplementasikan oleh CoroutineScheduler), tetapi memisahkan limit untuk tugas CPU dan IO. Saat sebuah coroutine Dispatchers.IO diblokade, runtime dapat meminjam thread lain — sehingga throughput IO tetap tinggi.

Demonstrasi Empat Dispatcher #

Kode berikut memperlihatkan thread tempat tiap dispatcher menjalankan coroutine:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("[main] thread=${Thread.currentThread().name}")

    // Dispatchers.Default: thread pool CPU
    launch(Dispatchers.Default) {
        println("[default] thread-pool=${namaPool(Thread.currentThread().name)}")
    }.join()

    // Dispatchers.IO: thread pool untuk IO blocking
    launch(Dispatchers.IO) {
        println("[io] thread-pool=${namaPool(Thread.currentThread().name)}")
    }.join()

    // Dispatchers.Unconfined: menjalankan di thread pemanggil
    launch(Dispatchers.Unconfined) {
        val sebelumDelay = Thread.currentThread().name
        delay(10)
        val setelahDelay = Thread.currentThread().name
        println("[unconfined] sebelum-delay=$sebelumDelay setelah-delay=$setelahDelay")
    }.join()

    // withContext: ganti dispatcher sementara
    val hasil = withContext(Dispatchers.Default) {
        println("[withContext] thread-pool=${namaPool(Thread.currentThread().name)}")
        100
    }
    println("[withContext] hasil=$hasil kembali-thread=${Thread.currentThread().name}")
}

fun namaPool(namaThread: String): String {
    return if (namaThread.startsWith("DefaultDispatcher")) "DefaultDispatcher"
    else if (namaThread.startsWith("DefaultExecutor")) "DefaultExecutor"
    else namaThread
}

Output yang Dihasilkan:

text
[main] thread=main
[default] thread-pool=DefaultDispatcher
[io] thread-pool=DefaultDispatcher
[unconfined] sebelum-delay=main setelah-delay=kotlinx.coroutines.DefaultExecutor
[withContext] thread-pool=DefaultDispatcher
[withContext] hasil=100 kembali-thread=main

Mari kita telaah baris-baris kode di atas:

  1. Baris 4 (runBlocking { ... }): di main(), runBlocking memakai thread main. Coroutine yang dimulai tanpa dispatcher eksplisit akan mewarisi dispatcher main ini.
  2. Baris 7-9 (launch(Dispatchers.Default)): coroutine dipindahkan ke thread DefaultDispatcher-worker-N. Fungsi helper namaPool() menyederhanakan output agar deterministik — worker-N bisa berbeda antar run, tetapi prefix DefaultDispatcher tetap sama.
  3. Baris 13-15 (launch(Dispatchers.IO)): coroutine dipindahkan ke thread DefaultDispatcher-worker-N juga — keduanya berbagi thread pool yang sama, hanya labelnya berbeda. Yang membedakan adalah limit dan jenis tugas yang diharapkan.
  4. Baris 19-23 (launch(Dispatchers.Unconfined)): coroutine menjalankan blok pertama di thread pemanggil (main). Saat menemui delay(10), coroutine ditangguhkan. Setelah delay, ia di-resume oleh DefaultExecutor (thread khusus untuk resume) — bukan kembali ke thread main. Inilah karakteristik Unconfined: thread bisa berpindah setelah titik suspend.
  5. Baris 26-29 (withContext(Dispatchers.Default)): builder withContext menjalankan blok di dispatcher baru, lalu kembali ke dispatcher pemanggil setelah selesai. Perhatikan baris [withContext] hasil=100 kembali-thread=main — setelah withContext selesai, eksekusi kembali di thread main, bukan di thread pool Default.

withContext untuk Pindah Dispatcher #

Builder withContext(dispatcher) { ... } adalah cara idiomatik untuk berpindah dispatcher sementara. Pola umum di kode backend:

kotlin Pola: suspend function yang pindah ke IO
suspend fun bacaFile(path: String): String = withContext(Dispatchers.IO) {
    File(path).readText()  // blocking IO, aman karena dispatcher IO
}

withContext adalah suspend function. Saat dipanggil, ia menangguhkan coroutine saat ini, menjalankan blok di dispatcher baru, lalu melanjutkan di dispatcher asal. Pemanggil tidak perlu tahu dispatcher mana yang dipakai — fungsi tampak seperti fungsi suspend biasa.

Dispatchers.Main #

Dispatchers.Main adalah dispatcher khusus platform. Di Android, ia merujuk ke thread UI; di Swing, ke event dispatch thread; di JVM plain, ia memerlukan dependency tambahan (mis. kotlinx-coroutines-swing). Pemakaian umum:

kotlin Pola umum: load data di IO, update UI di Main
suspend fun muatProfil(id: Int) {
    val data = withContext(Dispatchers.IO) { ambilDariServer(id) }
    withContext(Dispatchers.Main) {
        uiState.value = data  // aman update UI
    }
}

Anti-Pattern: Thread.sleep di Coroutine #

Kesalahan umum: memakai Thread.sleep di coroutine untuk simulasi delay. Thread.sleep memblokade thread OS — bukan menangguhkan coroutine. Jika dipakai di Dispatchers.Default bersama 10 coroutine lain, satu Thread.sleep(1000) akan menghabiskan satu dari beberapa thread yang tersedia.

kotlin Anti-pattern: Thread.sleep memblokade dispatcher
launch(Dispatchers.Default) {
    Thread.sleep(1000)  // BURUK: thread diblokade
}

launch(Dispatchers.Default) {
    delay(1000)  // BAIK: coroutine ditangguhkan, thread bebas
}

Referensi Lanjutan #

Selanjutnya #

Dengan memahami dispatcher, kita siap menelusuri lifecycle Job: bagaimana coroutine dilacak, dibatalkan, dan menunggu. Bab Job dan Cancellation akan membongkar Job, cancel(), join(), serta mekanisme pembatalan kooperatif.