Skip to Content
Course content

120: ZIO Fibers and Concurrency

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a colleague who was building a price aggregation engine. The goal was simple: hit five different vendor APIs, get the current price of a stock, and return the lowest one. He had written it using standard Java Futures. It worked, but as the load increased, the service started hitting "Too many open files" errors and the memory usage was spiking wildly. He was essentially spawning a heavy OS thread for every single vendor request across every single user request. He was terrified to touch it because the callback nesting had become a "pyramid of doom" that no one wanted to untangle.

This is where ZIO Fibers save your sanity. In the traditional world, a thread is an expensive resource managed by the operating system. In ZIO, we use Fibers. Think of them as "green threads" or lightweight logical threads. They aren't mapped 1:1 to OS threads; instead, the ZIO runtime manages thousands—or even millions—of them on a small pool of actual CPU threads. When a fiber hits a blocking operation (like an API call), ZIO simply parks that fiber and uses the underlying OS thread to do other work. When the API response comes back, the fiber is resumed exactly where it left off.

The Mechanics of Forking and Joining

The primary way you start a fiber is via ZIO.fork. When you fork an effect, you aren't just running it in the background; you're creating a Fiber object that acts as a handle to that background process. I like to think of fork as "fire and forget, but keep the receipt."

import zio._

val fetchPrice = ZIO.logInfo("Fetching price...") *> ZIO.sleep(1.second) *> ZIO.succeed(100.0)

val program = for {
  fiber <- fetchPrice.fork // This starts the effect in a new fiber immediately
  _     <- ZIO.logInfo("I'm doing other work while the price fetches!")
  price <- fiber.join       // This pauses the current fiber until the forked one finishes
} yield price

Notice that fiber.join is where we synchronize. If the fiber is already finished, join returns the value immediately. If it's still running, the current fiber suspends. This is vastly different from Await.result in Scala Futures, which blocks the actual OS thread and wastes resources. Here, we're just telling the ZIO runtime, "Wake me up when this specific fiber is done."

Managing Life Cycles with Interruption

One of the most powerful—and often overlooked—features of ZIO fibers is interruption. In a distributed system, you can't afford to let "zombie" requests run forever. If a user cancels a request or a timeout hits, you want to kill the background work immediately to free up resources.

Because fibers are first-class citizens, you can call fiber.interrupt. This doesn't just "stop" the code; it sends a signal to the fiber to shut down gracefully. If the fiber was in the middle of a ZIO.sleep or a network call, it stops right there. I've seen this save production environments from cascading failures where a slow downstream dependency would normally clog up every single available thread in the JVM.

val slowTask = ZIO.sleep(10.seconds) *> ZIO.succeed("Finally done!")

val program = for {
  fiber <- slowTask.fork
  _     <- ZIO.sleep(1.second)
  _     <- ZIO.logInfo("Too slow! Killing the task.")
  _     <- fiber.interrupt
} yield ()

Just a word of caution: interruption is cooperative. While ZIO's built-in effects are all interruptible, if you wrap a piece of legacy Java code that runs a while(true) loop without checking for interruption, the fiber will just hang there. Always ensure your long-running custom loops are "ZIO-aware" if you plan on killing them.




📋 Practical Task

Build a Competitive API Race Handler

You need to implement a "Race" mechanism. Imagine you have three different mirrored servers providing the same data. To minimize latency, you want to request the data from all three simultaneously and take the result from whichever one responds first. Once the first response arrives, you must immediately interrupt the other two fibers to avoid wasting server resources.

Requirements:

  • Create three effects (e.g., serverA, serverB, serverC) that simulate network latency using ZIO.sleep with random durations between 100ms and 1000ms.
  • Fork all three effects into separate fibers.
  • Use a mechanism to wait for the first one to complete.
  • Explicitly interrupt the remaining two fibers.
  • Log which server won the race and confirm that the others were stopped.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.