Image Source: https://bit.ly/3983OrC

What are fibers?


Fibers are workers, they run code & keep track of their progress. They are a concurrency mechanism like threads but are contrasting in nature when compared with threads. The control flow of a thread can get interrupted at any time and another thread can take over. With fiber, the control only switches when we explicitly tell it to. In this article, we will dive deeper to understand this difference and how to implement Fibers.

First things first, the difference between #fibers and #threads

  • Fibers are light-weight and they eat much less memory as compared to threads
  • You have control over fibers than threads.
  • Your operating system runs threads and decides when to run and pause but with fibers things are variant. We need to specify to a fiber when to start and when to stop.
  • Fibers are means of writing code blocks which can be paused or resumed, much like threads, but the difference is it's scheduling is done by the programmer and not by the operating system.
  • Threads run in the background doing their task, but when a fiber runs, it becomes your main program until you stop it.

Fibers are light-weighted threads, i.e. they are high performant in comparision to threads and provide full control to the programmer on when to stop or start them. Let us look at the performance graph for fibers and threads and understand how they perform.

Image source: https://bit.ly/35OjoXq

Two execution threads; one thread blocks for 40ms on an IO call and then takes 10ms to post-process the data; second thread needs 50ms of pure CPU time; vs same scenario implemented with Fibers and cooperative scheduling.

By default, Ruby MRI uses a fair scheduler, which means that each thread is given an equal time to run (10ms quantum) before it is suspended and the next thread is put in control. If one of your threads is inside a blocking call during those 10ms, think of it as time wasted - everyone is sleeping! By contrast, Fibers force the programmer to do explicit scheduling which can certainly add to the complexity of the program, but offer us the full flexibility of determining how our CPU resources are used and also help us avoid the need for locks in mutexes in our code! It can be seen that fibers are cheaper to create and perform better than threads.

How to implement fibers ?

When a fiber is created it doesn't run automatically. Rather, we need to explicitly ask it to run using the Fiber#resume method.

example:

f = Fiber.new { puts 1 }

f.resume 
1
=> nil

It will keep in the running state and control your main program until you stop it.

But how to stop a fiber ?

We can stop a running fiber by calling Fiber.yield method. ( It is separate from the yield method which is used to return blocks)

Example:

f = Fiber.new { puts 1; Fiber.yield; puts 2 }

f.resume
1
=> nil

f.resume
2
=> nil

The code that is running inside the fiber gives the control back to the caller by using Fiber.yield.

After yielding or termination, the fiber returns the last executed expression's value.

Example:

f = Fiber.new { puts 1; Fiber.yield; puts 2 }

f.resume
1
=> nil

f.resume
2
=> nil

f.resume
=> FiberError (dead fiber called)

In the above example, when we run f.resume it puts 1 to the screen and stops as it comes across the Fiber.yield method but it resumes again from where it left of when we again call f.resume and puts 2 on the console.

Calling the Fiber.yield acts like a pause button in your program, when to pause when to resume and that is why fibers are to handy.

Creating loops and endless sequences using fibers

We can use fibers to create an infinite sequence using Fiber.yield

All we need are Fibers, Loop and a Counter.

Fiber.yield can take arbitrary parameters or blocks to perform a specific task as well, let us take an example of creating a factorial using a fiber.

factorial =
Fiber.new do
  count = 1
  loop do
    Fiber.yield (1..count).inject(:*)
    count += 1
  end
end

You can call the resume method on this fiber factorial and use it as many times as you want to get the next number in the sequence.

For example:

Array.new(4) { factorial.resume }

=> # [1, 2, 6, 24]

A fiber allows you to write a code that can be paused & resumed at your own will and is highly performant as well.

This helpful SO answer states that

Fibers are something you will probably never use directly in application-level code. They are a flow-control primitive which you can use to build other abstractions, which you then use in higher-level code.

For more information on Ruby Fibers, refer this link.

Thanks for reading!