Menaklukkan eksekusi paralel di Scala: scala.concurrent.Future dengan ExecutionContext dan Await, lalu fiber-based concurrency ala cats-effect dan ZIO dengan Ref, Queue, dan structured concurrency yang lebih aman dan bisa dibatalkan.

Setelah di episode 11 kalian memahami effect system, sekarang kita berhadapan dengan masalah tersulit dalam pemrograman: menjalankan banyak hal bersamaan. Aplikasi modern — backend service, data pipeline — tidak bisa berjalan satu langkah demi langkah; mereka harus memproses ribuan request secara paralel.
Mengapa topik ini penting? Karena concurrency adalah tempat lahirnya bug paling menyakitkan: race condition, deadlock, dan data corruption. Scala menawarkan perjalanan dari Future klasik hingga fiber yang ringan — dan pemahaman yang benar tentang keduanya akan menentukan apakah aplikasi kalian tetap sehat saat trafik naik.
scala.concurrent.Future adalah titik masuk concurrency paling tradisional di Scala:
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global
val async: Future[Int] = Future {
Thread.sleep(500) // simulasi kerja berat
42
}Future { ... } menjalankan blok di thread pool (ExecutionContext) dan langsung mengembalikan nilai future — placeholder hasil yang akan datang.
Future bisa dikomposisi seperti collection:
val result: Future[Int] =
for
a <- Future { 10 }
b <- Future { 20 }
yield a + bKedua Future di atas bisa berjalan paralel (jika eksekusi memungkinkan), dan hasilnya digabung tanpa blocking.
Untuk program sederhana, Await memblokir sampai selesai:
import scala.concurrent.Await
import scala.concurrent.duration.*
val value = Await.result(result, 5.seconds)
println(value) // 30Warning
Await memblokir thread. Di server production, memblokir thread pool adalah salah satu penyebab degradasi tercepat. Await hanya untuk entry point dan scripting — bukan untuk logika di dalam service.
Future punya kelemahan struktural:
failure callback, mudah terlewat.Untuk program sederhana, ini cukup. Untuk aplikasi produksi, fiber adalah jawaban modern.
Fiber adalah unit komputasi yang sangat ringan — ribuan fiber bisa berjalan di atas beberapa thread. Ini beda fundamental dari Future yang 1:1 dengan thread.
import cats.effect.{IO, IOApp}
object Main extends IOApp.Simple:
val run: IO[Unit] =
for
fiber1 <- IO.sleep(500.millis).map(_ => "tugas A").start
fiber2 <- IO.sleep(200.millis).map(_ => "tugas B").start
a <- fiber1.joinWithNever
b <- fiber2.joinWithNever
_ <- IO.println(s"$a dan $b selesai")
yield ().start meluncurkan fiber; .joinWithNever menunggu hasilnya. Perbedaannya dengan Future:
fiber.cancel menghentikan secara bersih.Prinsipnya: fiber yang dibuat di dalam scope tidak boleh hidup lebih lama dari scope-nya. Contoh dengan parTraverse yang memproses koleksi secara paralel:
import cats.effect.IO
import cats.syntax.all.*
val tasks: List[IO[Int]] = List(1, 2, 3, 4, 5).map(n => IO.pure(n * 10))
val all: IO[List[Int]] = tasks.parSequence
all.map(result => println(result))parSequence menjalankan semua efek paralel dan menunggu semuanya; jika salah satu gagal, yang lain dibatalkan — perilaku terstruktur yang jauh lebih mudah dinalar daripada Future liar.
Ref adalah cell immutable yang menyediakan update atomik — pengganti var di dunia effect:
import cats.effect.{IO, Ref}
val program: IO[Int] =
for
counter <- Ref.of[IO, Int](0)
_ <- List.fill(100)(counter.update(_ + 1)).parSequence
value <- counter.get
yield value // selalu 100100 fiber meng-update counter secara paralel, dan Ref.update menjamin tidak ada race condition — hasilnya deterministik 100.
Queue menghubungkan producer dan consumer:
import cats.effect.IO
import cats.effect.std.Queue
val program: IO[Unit] =
for
queue <- Queue.unbounded[IO, Int]
producer = (1 to 5).toList.traverse_(queue.offer)
consumer = queue.take.flatMap(n => IO.println(s"terima: $n"))
_ <- producer.start
_ <- consumer.replicateA(5).start
_ <- IO.sleep(1.second)
yield ()offer menaruh, take mengambil (memblokir fiber jika kosong — bukan thread!). Pola ini adalah tulang punggung pipeline streaming.
ZIO juga berbasis fiber dan structured concurrency, dengan API yang sedikit berbeda:
import zio.*
object Main extends ZIOAppDefault:
def run =
for
fiber <- ZIO.succeed("tugas berat").fork
hasil <- fiber.join
_ <- Console.printLine(s"hasil: $hasil")
yield ()fork dan join di ZIO setara dengan .start dan .join di cats-effect. ZIO juga punya ZIO.foreachPar untuk paralelisme terstruktur.
Note
Aturan praktis 2026: untuk kode produksi baru, mulai dari fiber-based concurrency (cats-effect/ZIO) langsung — bukan Future. Future tetap perlu dipahami untuk membaca codebase lama dan memakai API yang berbasis JVM.
Dua konsep yang sering tertukar:
Scala mendukung keduanya: concurrency lewat fiber/future, parallelism lewat thread pool dan collection paralel (mis. .par).
Await di dalam service — blokir thread = bunuh throughput. Efek harus dikomposisi tanpa blocking.Ref — var yang dibagikan antar fiber adalah undangan race condition.Inti yang harus dibawa pulang:
Future adalah concurrency klasik 1:1 dengan thread, punya masalah: tidak bisa dibatalkan, Await memblokir.parSequence, foreachPar) membuat lifecycle fiber terprediksi dan aman.Ref untuk state atomik, Queue untuk komunikasi antar fiber.Di episode 13 selanjutnya, kita memakai semua ini untuk membangun sesuatu yang nyata: backend service dengan http4s dan Tapir — HTTP server pure functional, endpoint type-safe, dokumentasi OpenAPI, dan serialisasi JSON dengan circe. Sampai jumpa di episode 13!