Concurrency is essential for every application to achieve higher performance within the limited set of CPU cores. In Ruby, Concurrency can be implemented in a variety of ways, including using Threads or Fibers. Ruby Fibers allow the programmer to control when to start and stop the process, providing greater flexibility and performance. To know more about Ruby Fibers, check out our previous blog An Introduction to Ruby's 'Fibers'.

An Introduction to Ruby’s ‘Fibers’
What are fibers ? How can we use them for increasing the app performance in Ruby.

Ruby 3 was released with the primary goal of improving performance. Fiber::SchedulerInterface was added in Ruby 3 to support the concept of non-blocking fiber.

Fiber Scheduler interface is a set of hooks for blocking operations and allows inserting a asynchronous behavior when a blocking operation occurs.

With this new interface, we can split the long I/O operations into separate fiber hooks, and instead of us controlling the fiber execution, the Scheduler will manage the waiting and resuming of the fiber when it is ready.

To enable async behaviour in our application, we need to set the Fiber scheduler object using Fiber.set_scheduler(scheduler) method. Ruby does not include a default scheduler, so we can create our own or use one from the list of available schedulers here.

In this article, we will see how using the Fiber Scheduler interface we can perform asynchronous programming in our application. In the below example I will be using Ruby 3.1 with fiber_scheduler gem as the scheduler.

Assume that for an action, we need to retrieve data from multiple APIs or run time-consuming DB queries. When multiple long I/O operations must be performed, they can be placed in separate non-blocking Fibers rather than blocking each other while waiting.

require 'net/http'
require 'fiber_scheduler'

Benchmark.realtime do
  Thread.new do # in this thread, we'll have non-blocking fibers
    Fiber.set_scheduler(FiberScheduler.new)

    %w[India Germany Canada].each do |country|
      Fiber.schedule do # Runs block of code in a separate Fiber
        t = Time.now
        # Instead of blocking until the response is ready, the Fiber will invoke scheduler
        # to add itself to the list of waiting fibers and transfer control to other fibers
        Net::HTTP.get('universities.hipolabs.com', "/search?country=#{country}")
        puts '%s: finished in %.3f' % [country, Time.now - t]
      end
    end
  end.join # At the END of the thread code, Scheduler will be called to dispatch all waiting fibers 
           # in a non-blocking manner
end

Canada: finished in 0.631
Germany: finished in 0.801
India: finished in 0.863
=> 0.8634749999982887 # total time taken

The code above generates three fibers, each of which makes an HTTP request. Rather than waiting for the request to be completed, the fibers transfer the control to the next fiber. As a result, the total execution time is less than the sum of the execution times of the individual fibers.

We can also use FiberScheduler to add nested fiber blocks and specify whether the parent should wait/block until the execution of the child is complete.

  1. Waiting: Sometimes we may need parent fiber to wait for the child fiber to complete.
    FiberScheduler do
      Fiber.schedule do # parent
        Fiber.schedule(:waiting) do # child
          # Our code goes here
          # for simplicity I have used sleep
          sleep 2
        end
        # The fiber stops here until the waiting child fiber completes.
    
        sleep 2
      end
    
      Fiber.schedule do
        sleep 2
      end
    end
    
    # The above example will take 4 seconds to finish.
    
  2. Blocking: This will prevent all other fibers from running until the current one is finished.
    FiberScheduler do
      Fiber.schedule do
        # the fiber will block all the execution until the below fiber completes.
        Fiber.schedule(:blocking) do
          sleep 2
        end
      end
    
      Fiber.schedule do
        sleep 2
      end
    end
    
    # The above example will take 4 seconds to finish.
    

Final Thoughts

Adding the interface for Fiber Scheduler, is one of Ruby 3's most significant additions. By leveraging Ruby's built-in methods set_scheduler and schedule {...}, one may easily get the benefits of concurrency by parallelizing long-blocking operations without any overhead of managing the fibers.

Happy coding with Fiber Scheduler! ❤️

References