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'.
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.
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.
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! ❤️