Banyak developer memakai istilah concurrency dan parallelism bergantian seolah bermakna sama. Akibatnya, diskusi tentang coroutines sering rancu: apakah “coroutine berjalan paralel” atau “coroutine berjalan konkuren”? Chapter ini memisahkan dua konsep tersebut secara presisi dan menunjukkan bagaimana coroutines Kotlin dapat memenuhi kedua model — concurrency tanpa parallelism, concurrency dengan parallelism, hingga parallelism tanpa concurrency. Pemahaman ini menjadi landasan untuk memilih dispatcher yang tepat pada chapter berikutnya.

Definisi Operasional #

Concurrency adalah kemampuan sebuah program untuk menangani banyak tugas pada saat yang bersamaan secara logis. Tugas-tugas tersebut tidak harus berjalan serentak secara fisik — yang penting program mampu beralih di antara tugas dan membuat kemajuan pada semuanya. Sebuah thread tunggal yang melayani banyak koneksi IO secara bergantian adalah contoh concurrency murni.

Parallelism adalah kemampuan untuk menjalankan banyak tugas pada saat yang bersamaan secara fisik. Hal ini memerlukan beberapa unit eksekusi (CPU core, thread OS). Sebuah program yang membagi komputasi numerik ke empat core sehingga masing-masing menghitung seperempat data adalah contoh parallelism.

Perbedaan inti dirangkum pada tabel berikut:

AspekConcurrencyParallelism
UnitLogis (struktur program)Fisik (eksekusi simultan)
Butuh banyak core?TidakYa, untuk parallelism sejati
Tujuan utamaMenangani banyak tugas (IO, event)Mempercepat komputasi CPU-bound
Contoh di Kotlinasync/launch tanpa thread baruDispatchers.Default pada multi-core

Rob Pike menyatakan dengan ringkas: concurrency is about dealing with lots of things at once; parallelism is about doing lots of things at once.

Concurrency pada Satu Thread #

Coroutines tidak memerlukan banyak thread untuk bersifat konkuren. Sebuah runBlocking pada main thread dapat menjalankan beberapa async yang masing-masing menangguhkan diri pada titik delay. Selama penangguhan, thread utama bebas mengeksekusi coroutine lain.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("== Concurrency pada 1 thread ==")
    // Tiga coroutine dijalankan secara konkuren tanpa parallelism
    val a = async(Dispatchers.Unconfined) { cetakDanKembalikan("A", 1) }
    val b = async(Dispatchers.Unconfined) { cetakDanKembalikan("B", 2) }
    val c = async(Dispatchers.Unconfined) { cetakDanKembalikan("C", 3) }
    println("hasil: ${a.await()} ${b.await()} ${c.await()}")

    println()
    println("== Parallelism pada banyak thread ==")
    // Empat async di Dispatchers.Default berjalan paralel
    val tugas = (1..4).map { i ->
        async(Dispatchers.Default) {
            delay(200)
            i * 10
        }
    }
    val total = tugas.awaitAll().sum()
    println("total=$total")
}

suspend fun cetakDanKembalikan(label: String, nilai: Int): Int {
    println("  -> $label mulai")
    delay(50)
    println("  <- $label selesai")
    return nilai
}

Output yang Dihasilkan:

text
== Concurrency pada 1 thread ==
  -> A mulai
  -> B mulai
  -> C mulai
  <- A selesai
  <- B selesai
  <- C selesai
hasil: 1 2 3

== Parallelism pada banyak thread ==
total=100

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-8 (blok concurrency): tiga async dijadwalkan berurutan dengan Dispatchers.Unconfined. Saat sebuah async dijadwalkan, ia langsung berjalan di thread pemanggil sampai menemui titik suspend (delay(50)). Setelah suspend, eksekusi kembali ke runBlocking dan coroutine berikutnya dimulai.
  2. Pola output “mulai A, mulai B, mulai C, selesai A, selesai B, selesai C”: membuktikan ketiga coroutine berjalan bersamaan secara logis. Mereka memulai hampir bersamaan lalu selesai hampir bersamaan — semua pada satu thread main.
  3. Baris 12-17 (blok parallelism): async(Dispatchers.Default) memindahkan eksekusi ke thread pool bawaan (biasanya jumlahnya sama dengan core CPU). Empat tugas dengan delay(200) dijadwalkan paralel.
  4. awaitAll().sum(): menunggu semua hasil lalu menjumlahkan. Karena paralel, total durasi mendekati 200ms (bukan 800ms). Output yang dicetak hanyalah total=100 karena delay tidak menghasilkan output antara.
  5. Dispatchers.Unconfined: dipakai khusus untuk demo concurrency pada satu thread. Dispatcher ini tidak memindahkan eksekusi ke thread lain — coroutine tetap di thread pemanggil. Untuk kode produksi, gunakan dispatcher lain yang akan kita bahas pada chapter Dispatchers.

Tiga Kombinasi yang Mungkin #

Pemahaman concurrency dan parallelism menghasilkan tiga kombinasi praktis:

KombinasiContohCatatan
Concurrency tanpa parallelismUI thread + banyak asyncCocok untuk aplikasi UI; thread utama tetap responsif
Concurrency dengan parallelismMulti-platform server dengan Dispatchers.DefaultMaksimalisasi throughput CPU + IO
Parallelism tanpa concurrencyWorker thread pool memproses antrian tugasCocok untuk batch processing data

Coroutine Kotlin mendukung ketiganya dengan satu model kode — yang berubah hanya pemilihan dispatcher dan struktur scope. Tidak ada perubahan sintaksis: async tetap async, await tetap await.

Jebakan Umum #

Sering dianggap bahwa “lebih banyak coroutine = lebih cepat”. Kenyataannya, jika semua coroutine memakai Dispatchers.Main (satu thread), tidak ada parallelism sama sekali — hanya concurrency. Kode CPU-bound seperti sorting atau parsing JSON besar tidak akan dipercepat dengan menambah coroutine pada dispatcher Main; sebaliknya, ia justru melambat karena overhead penjadwalan.

Referensi Lanjutan #

Selanjutnya #

Setelah memahami concurrency dan parallelism, langkah berikutnya adalah melihat langsung konstruksi dasar coroutines: runBlocking, launch, dan await — yang akan kita bedah pada chapter Coroutine Fundamentals.