Skip to Content
Course content

99: Fibers Revisited In Depth

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

We've already touched on Fibers as a way to pause and resume execution, but most developers stop there. They treat Fibers like a fancy version of a generator or a way to implement a cooperative state machine. If that's all you're doing, you're barely scratching the surface. The real power of Fibers isn't in the manual resume and yield calls—it's in how they enable non-blocking I/O without turning your codebase into a nested mess of callbacks.

The Manual Orchestration Trap

When I first started using Fibers, I did what most people do: I tried to be the conductor. I'd wrap a few I/O tasks in Fibers and write a while loop that manually called resume on each one until they all finished. It felt clever because I was avoiding threads, but I quickly realized I had just rebuilt a very poor version of an event loop.

# The "I'll manage it myself" approach
fibers = urls.map do |url|
  Fiber.new do
    # Imagine this is a blocking network call
    result = Net::HTTP.get(URI(url)) 
    Fiber.yield result
  end
end

results = []
while fibers.any?(&:alive?)
  fibers.each do |f|
    results << f.resume if f.alive?
  end
end

Here is the problem: Net::HTTP.get is a blocking call. Even though it's inside a Fiber, it blocks the entire Ruby thread. The Fiber doesn't magically make the network call asynchronous; it just gives you a place to pause. In the example above, the loop doesn't actually provide concurrency. It just executes the requests sequentially, pausing and resuming them in a way that adds overhead without any of the performance gains. You're paying the "complexity tax" of Fibers without getting the "speed dividend."

Letting the Scheduler Handle the Wait

The breakthrough came in Ruby 3.0 with the Fiber Scheduler. Instead of you manually deciding when to yield and resume, Ruby can now hook into the I/O operations themselves. When a Fiber hits a blocking operation (like reading from a socket), it automatically yields control back to the scheduler, which then resumes another Fiber that is actually ready to do work.

This is a fundamental shift in responsibility. You stop worrying about the mechanics of the pause and start focusing on the logic of the concurrency. I usually recommend using a gem like async to provide the scheduler implementation, because writing a production-ready scheduler from scratch is a rabbit hole you probably don't want to fall down.

require 'async'
require 'net/http'

Async do
  urls.each do |url|
    Async do
      # This looks blocking, but the scheduler intercepts it!
      response = Net::HTTP.get(URI(url))
      puts "Fetched #{url}: #{response.size} bytes"
    end
  end
end

The Trade-off: Determinism vs. Magic

Now, you might ask why we wouldn't just do this every time. The trade-off is transparency. When you're manually resuming Fibers, you have absolute, deterministic control over the execution order. You know exactly who runs and when. With a scheduler, you're introducing a layer of "magic." The runtime decides the most efficient way to switch contexts based on which socket becomes readable first.

For 99% of I/O-bound applications—web scrapers, API aggregators, or chat servers—the scheduler is the correct choice. It removes the boilerplate and prevents the "blocking the world" bug that plagues naive Fiber implementations. You get the performance of an event loop (like Node.js) but you get to write your code in a straight, synchronous line. That's the real win here: avoiding the "callback hell" while keeping the efficiency of non-blocking I/O.




📋 Practical Task

Building a Non-Blocking API Health Monitor

Your goal is to create a script that checks the health of multiple external APIs simultaneously. You must avoid the "Manual Orchestration" pattern and instead use a Fiber-based scheduler to ensure that one slow API doesn't block the checks for the others.

Requirements:

  • Use the async gem (or Fiber.set_scheduler if you are implementing a custom one) to wrap your requests.
  • Create a list of at least five different URLs (you can use https://httpbin.org/delay/1, /delay/2, etc., to simulate slow responses).
  • The script should initiate all requests concurrently.
  • Each Fiber should print the URL and the time it took to complete the request.
  • Verify that the total execution time of the script is roughly equal to the slowest single request, rather than the sum of all requests.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.