With the release of Ruby 3.0, two concurrecy features/frameworks were introduced, Fibers for light-weight 🥊 concurrency, and Ractors for full-weight 🥊 🥊 parallel execution.

In this post, let’s cover the Fiber light-weight concurrency framework, how the async gem extends Fiber for ease of use, and let’s have some fun by running benchmarks.

Using the Fiber primitive

Using the Fiber primitive, let’s write a sample blocking fiber program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env ruby

# asynchronous - using blocking Fiber primitive
def nap
    puts "you now have to wait, while I nap..."
    sleep 3
    puts "...and now I'm awake, you can move along."
end

puts "blocking asynchronous execution, using the Fiber primitive"
fiber = Fiber.new do
    nap
    nap
end

fiber.resume
puts "blocking fibers are so boring."

and run it:

1
2
3
4
5
6
blocking asynchronous execution, using the Fiber primitive
you now have to wait, while I nap...
...and now I'm awake, you can move along.
you now have to wait, while I nap...
...and now I'm awake, you can move along.
blocking fibers are so boring.

As you can see, despite using the Fiber primitive for asynchronous code execution, by default, the behavior is blocking in nature. In other words, the code is run in serial fashion and pauses the current thread the Ruby interpreter is running on until the fiber object finishes execution. Lame.

Well, how do we use the Fiber primitive in a non-blocking manner, so as to mimic actually concurrent execution of code? We have to implement a Fiber scheduler interface within the context of the Ruby interpreter thread. But think about it, nobody wants to do that! (it also means everyone has to, which isn’t very DRY in principle)

Instead, let’s leave it to the pros, and use a Ruby gem that already implements a Fiber schedule interface for us and makes writing non-blocking Fiber enabled code a breeze…

Using the Async gem

Meet the Async gem, which implements the Fiber scheduler interface for us. With the Asynch gem, we can write non-blocking code like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env ruby

# asynchronous - non-blocking fiber using Async gem
require 'async'

def nap_async
    Async do |task|
        puts "you now have to wait, while I nap..."
        sleep 3
        puts "...and now I'm awake, you can move along."
    end
end

puts "non-blocking asynchronous execution, using the Async gem"

Async do
    nap_async
    nap_async
end

puts "non-blocking fibers are so much fun!"

and run it:

1
2
3
4
5
6
non-blocking asynchronous execution, using the Async gem
you now have to wait, while I nap...
you now have to wait, while I nap...
...and now I'm awake, you can move along.
...and now I'm awake, you can move along.
non-blocking fibers are so much fun!

I don’t know about you, but I sure do like writing my code in non-blocking Fiber via Async and enjoying the benefits of asynchronous performance.

Let’s have some fun, with benchmarks!

Running the following benchmark…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env ruby

require 'async'
require 'benchmark'

def nap
    sleep 3
end

def nap_async
    Async do |task|
        sleep 3
    end
end

Benchmark.bm do |x|
  # sequential version
  x.report('sequential'){ 3.times{ nap } }

  # blocking fiber version
  x.report('blocking fiber'){
    3.times.map do
        fiber = Fiber.new do
            nap
        end

        fiber.resume
    end
  }

  # non-blocking fiber version
  x.report('non-blocking fiber'){
    Async do 
      3.times.map do
        Async do
            nap_async
        end
      end
    end
  }
end

Generates the following results:

1
2
3
4
                    user       system     total    real
sequential          0.000245   0.000205   0.000450 (9.003599)
blocking fiber      0.000451   0.000198   0.000649 (9.003754)
non-blocking fiber  0.001503   0.000343   0.001846 (3.001658)

If you look at the real column values, which are in seconds, I hope you can see how great it is to write code with non-blocking fibers!

If you’ve liked what you’ve read and seen here, and would like to read more (better?) examples, go read this great intro to Async Ruby.