Model Konkurensi: Thread, Async, Actor, CSP
π Topik: Empat paradigma konkurensi inti β preemptive threads, cooperative async/event loop, actor model, CSP β dan fondasi konseptual yang membedakannya.
π― Tujuan Belajar
Setelah sesi ini:
- Bisa pilih runtime konkurensi berdasarkan workload shape (CPU-bound vs IO-bound vs stateful-distributed), bukan berdasarkan hype bahasa.
- Bisa jelaskan kenapa async tidak otomatis lebih cepat dari thread β dan kapan justru lebih lambat.
- Bisa bedakan shared-memory + lock dari message-passing sebagai keputusan arsitektural, bukan style.
- Bisa prediksi failure mode konkurensi sebelum ke production (race, deadlock, starvation, memory bloat, backpressure collapse).
π§ Penjelasan Inti β Kenapa Ada Banyak Model?
Konkurensi = banyak unit eksekusi logis maju bersamaan. Pararelisme = benar-benar jalan serentak di core berbeda. Konkurensi soal struktur, pararelisme soal hardware. Satu program concurrent belum tentu parallel.
Tiga masalah fundamental yang harus dipecahkan setiap runtime konkurensi:
- Scheduling β siapa jalan sekarang? Siapa preempt siapa?
- Komunikasi / state sharing β unit eksekusi tukar data bagaimana tanpa korup?
- Blokir β saat nunggu IO, siapa yang dikorbankan?
Setiap model di bawah = jawaban berbeda untuk tiga pertanyaan itu. Tidak ada jawaban universal.
βοΈ Cara Kerja Internal
1. Preemptive Threads (OS Thread / Kernel Thread)
- Scheduler = kernel. Preemption pakai timer interrupt. Thread bisa dihentikan kapan saja, di titik mana saja (termasuk di tengah operasi).
- Tiap thread punya stack sendiri (default ~1β8 MB) + register set = ~MB biaya per thread. Context switch = syscall + flush TLB + cache miss.
- Komunikasi: shared memory + sinkronisasi eksplisit (
mutex,semaphore,condvar, atomics). - Blokir: syscall blocking parkirkan thread. 10.000 thread blocking = 10.000 kernel task = memory + scheduler overhead boom.
2. Async / Event Loop (cooperative, single-threaded core)
- Satu (atau beberapa, = worker count) event loop. Runtime pegang queue task. Task jalan sampai menyerahkan kontrol eksplisit (
await,yield) atau selesai. - Blokir IO = daftarkan fd ke
epoll/kqueue/IOCP, suspensi coroutine, simpan continuation (state machine), loop lanjut task lain. Nanti IO siap β ready-queue. - Biaya per task: ratusan byte sampai beberapa KB (continuation), bukan MB. Jutaan task feasible.
- Blokir: tidak ada preemption. Kalau task pakai CPU 50ms tanpa
awaitβ seluruh loop beku. Ini cooperative. - Komunikasi antar task: shared memory juga, tapi karena single-thread-per-loop, banyak race hilang selama tidak ada
awaitdi antara read-write. Masih race lintas worker.
3. Actor Model (Erlang/OTP, Akka, Actix)
- Unit = actor: punya mailbox (queue), state privat, jalan satu pesan per satu (single-threaded semantics per actor).
- Tidak ada shared state. Komunikasi = kirim pesan async ke mailbox. Actor proses pesan satu per satu sampai mailbox kosong, baru ambil berikutnya.
- Lokasi-transparan: actor bisa local atau remote, pengiriman pesan sama. Ini yang bikin distribusi jadi seamless (Erlang).
- Failure model built-in: actor bisa crash independen, supervisor memutuskan restart/stop/escalate (let-it-crash). Ini bagian arsitektur, bukan cuma library.
4. CSP β Communicating Sequential Processes (Go channels, occam, Clojure core.async)
- Proses = sekuensial. Komunikasi lewat channel (typed, bisa unbuffered/buffered). Tanpa identitas penerima β channel yang jadi perantara, bukan alamat actor.
- Pola kunci:
selectnon-deterministik (pilih channel mana siap), timeout, done-channel. "Share memory by communicating, not communicate by sharing memory." - Send/receive bisa blocking (rendezvous) atau buffered β ini yang jadi flow control natural.
Perbedaan Actor vs CSP (sering dikacaukan): | Aspek | Actor | CSP | |---|---|---| | Target pesan | Alamat actor (identity) | Channel (rendezvous point) | | Buffering | Mailbox tak terbatas (default) | Channel eksplisit, bisa unbuffered | | Coupling | Sender tahu receiver | Sender hanya tahu channel | | Blocking | Send biasanya non-blocking | Send bisa blocking (backpressure) | | Failure | Supervisor hierarchy | Manual (context, error channel) |
ποΈ Pseudocode β Sama Workload, Empat Model
# 1. THREADS (shared mem + lock)
data = {}; lock = Mutex()
def worker(k):
v = fetch(k) # blocking syscall, thread parkir
lock.acquire(); data[k] = v; lock.release()
spawn N threads -> join # N besar = memory/switch boom
# 2. ASYNC (single loop, cooperative)
async def worker(k):
v = await fetch(k) # suspensi di await, loop lanjut
data[k] = v # aman selama tidak ada await di sini
# lock TIDAK wajib intra-loop, WAJIB lintas loop/thread
gather(worker(k) for k in keys)
# 3. ACTOR (mailbox, no shared state)
class Fetcher(Actor):
state = {}
def on_receive(msg):
if msg == "fetch": v = fetch_io(msg.k); reply(msg.sender, v)
if msg.starts_with("result"): state[msg.k] = msg.v
# banyak actor, mailbox queue, supervisor restart kalau crash
# 4. CSP (channel)
results = Channel()
def worker(k, out): out <- {k: fetch(k)} # blocking send = backpressure
for k in keys: go worker(k, results) # spawn goroutine
for _ in keys: merge(<-results) # rendezvous
βοΈ Trade-off & Kapan JANGAN Dipakai
| Model | Pakai kalau | JANGAN pakai kalau |
|---|---|---|
| Threads | CPU-bound; butuh true parallel; library blocking warisan; jumlah unit kecilβsedang (< ratusan) | 10K+ koneksi konkuren; IO-bound dominan; stack MB per unit = mematikan |
| Async | IO-bound masif (10K+ koneksi); latency rendah; throughput koneksi tinggi | Ada kode blocking tersembunyi (CPU heavy, lib sync) di jalur loop; tim belum paham cooperative trap; debug stack kompleks |
| Actor | Stateful, long-lived entity; butuh fault isolation & distribute lintas node; sistem yang tumbuh jadi terdistribusi | Operasi sederhana tanp state; butuh latensi super rendah per call; tim belum siap konsep supervisor/mailbox/dead letter |
| CSP | Pipeline / fan-out fan-in / worker pool; orkestrasi aliran data; want composable concurrency | Butuh alamat entity stateful yang hidup lama; butuh distribusi lintas node native (goroutine tidak pindah mesin) |
Klarifikasi krusial β async β cepat:
async menang saat IO-bound karena 1 thread layani 10K koneksi parkir. Kalau workload CPU-bound, async malah merugikan: tasks serialized di core, no true parallel, plus overhead state machine. CPU-bound β thread/process pool tetap jawaban. Banyak sistem hybrid: async untuk network IO + thread/process pool untuk CPU work (Go: goroutine + worker pool; Rust: tokio + spawn_blocking; Python: asyncio + run_in_executor).
Klarifikasi β thread model mana yang dipakai Go?
Go = goroutine = konsep mirip CSP green thread (M:N scheduling: M goroutine ke N OS thread). Bukan OS thread 1:1. Preemptive sejak Go 1.14 (async preemption via signal) β jadi bukan cooperative murni lagi. Channel = primitive CSP. go keyword spawn, channel komunikasi. Tapi Go juga izinkan shared memory + mutex β jadi hybrid.
Rust: async = stackless coroutine (state machine di-compile, zero-cost abstraction). Tidak ada runtime default β pilih tokio/async-std/smol. No GC β ownership model gantikan race detection runtime. Model = async + threads + channel (std::sync::mpsc, crossbeam, dan CSP-style via flume).
π Contoh Produksi & Failure Mode
-
Blocking di event loop (async) β Node.js: satu
fs.readFileSyncatau crypto hash di handler = seluruh server freeze untuk semua klien. Gejala: latency spike semua request sekaligus, CPU 100% di satu core. Fix:spawn_blocking/ worker thread / pindah ke native async API. -
Thread explosion β Java/C#: request-scoped thread tanpa pool. Traffic naik β ribuan thread blocking β OOM + massive context switch. Fix: thread pool + async IO, atau virtual threads (Java 21 Loom) / goroutine-style.
-
Deadlock lock ordering β Dua thread acquire lock A lalu B vs B lalu A. Klasik, masih terjadi di 2026. Deteksi:
jstack, Go-race/deadlock detector, Rust belum ada runtime deadlock detect. Fix: global lock ordering, atau hindari shared mutable state. -
Mailbox unbounded growth (Actor) β Producer lebih cepat dari consumer, mailbox tak dibatasi β memory bloat β akhirnya OOM. Ini tidak ada backpressure by default di actor model. Erlang/Akka punya mailbox limit (harus diset). Pelajaran: default mailbox unbounded = bug latency.
-
Bounded queue jadi bottleneck (CSP) β channel pendek + consumer lambat β sender blocking β throughput turun, latency naik. Ini mau tidak mau backpressure β bukan bug, tapi harus dipahami sebagai choke point. Unbuffered channel = full rendezvous = sinkron penuh.
-
Priority inversion / starvation (async) β Task CPU-heavy diselipkan di loop β task lain starvation karena tidak ada preemption. Cooperative model balas dendam.
-
Jones teknis: Erlang "let it crash" β actor crash bukan bencana kalau supervisor desain benar. Di thread/shared-memory, crash satu unit jatuhkan semuanya (whole process). Ini alasan fault-isolation actor model mahal nilai di sistem uptime tinggi (telekomunikasi, WhatsApp lama).
β οΈ Miskonsepsi Umum
-
"Async lebih cepat" β Salah sebagai pernyataan umum. Async menambah throughput untuk IO-bound konkuren. Untuk CPU-bound, lebih lambat (no parallelism + overhead state machine). Selalu tanya: workload shape apa?
-
"Threads itu obsolete" β Salah. CPU-bound, blocking lib warisan, dan true parallelism tetap butuh thread. Async tidak menggantikan thread; ia melengkapi. Banyak stack produksi jalan keduanya.
-
"Actor otomatis skala ke mana saja" β Salah. Actor local gratis; actor remote kena network partition, serialization cost, at-least-once delivery, ordering loss antar node. "Sama" hanya di API, bukan di failure semantics.
-
"Channel replay" / "Actor bebas race" β Salah. Tanpa shared state, race state hilang di dalam actor, tapi race antar actor tetap ada (urutan pesan, akses resource eksternal seperti DB). Concurrency safety β distributed safety.
-
"Cooperative = mudah debug" β Terbalik. Async stack trace terfragmentasi (stack tersebar di heap sebagai continuation), sulit dibaca. Thread stack lebih intuitif. Trade-off: async hemat resource, tapi debug mahal.
-
"Go preemptive seperti OS" β Setengah benar sejak 1.14 ada async preemption, tapi bukan sekuat OS scheduler. Potongan loop tak terpreempt (asm/cgo) tetap bisa stall. Dan izinkan blocking syscall β runtime detach M, jadi tidak sama dengan cooperative murni.
β Cek Pemahaman
- Server kamu 10K koneksi WebSocket idle, tiap koneksi sesekali kirim pesan. Throughput rendah, konkurensi tinggi. Model mana? Kenapa thread model pilihan buruk di sini?
- Service kamu hitung hash Argon2 per request (CPU-heavy 100ms). Kamu pakai Node.js async. Apa yang terjadi di bawah beban? Apa fix arsitekturnya?
- Kamu desain sistem dengan 1 juta entitas stateful (game/chat) yang hidup lama, butuh restart independen saat error. Actor atau CSP? Justifikasi pakai failure model, bukan preferensi idiomatik.
π Poin Kunci
- Tiga pertanyaan kunci setiap model: scheduling, komunikasi, blokir. Model = jawaban atas itu.
- Thread = preemptive, shared memory, ~MB/unit, true parallel. CPU-bound & legacy blocking.
- Async = cooperative, single-loop core, ~KB/unit, IO-bound masif. Blocking = racun loop.
- Actor = mailbox, state privat, lokasi-transparan, supervisor failure model. Stateful distribusi.
- CSP = channel, rendezvous, backpressure alami, no identity. Pipeline & worker pool.
- Async β cepat. Pilih berdasarkan workload shape, bukan tren.
- Default mailbox unbounded = bug latency (Actor). Buffered channel = backpressure tersembunyi (CSP).
- Shared memory + lock = model komunikasi, bukan sekadar API. Message passing bukan perak, keduanya punya failure mode.
β‘οΈ Eksplorasi Lanjut
- Structured concurrency β Kotlin coroutines, Swift structured concurrency, Python
TaskGroup(3.11+), Java Loom StructuredTaskScope. Kenapa "task tak punya parent" = source leak & error swallowing? - Virtual threads (Java 21 Loom) & green threads β M:N scheduling di runtime, bukan OS. Trade-off vs event loop.
- Backpressure sebagai first-class design β bounded queue, reactive streams (Reactive Streams spec), Kafka consumer lag, load shedding. Sambung ke saga & event-driven.
- Consistency model konkurensi β memory model bahasa (C++11 memory model, Java JMM happens-before). Kenapa
volatile/ atomic ordering bukan detail mikro. - Runtime konkurensi lintas bahasa β tokio (Rust), asyncio (Python), libuv (Node), GMP (Go), BEAM (Erlang). Bandingkan scheduling & preemption.
Konsep fundamental; tidak ada data versi terikat waktu yang rentan berubah. Verifikasi versi spesifik runtime (Loom, tokio, Go preemption) dilakukan saat implementasi.