Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby fiber: resuming transferred fibers

Tags:

ruby

fiber

fibers

I am trying to understand the behavior of the following code snippet. My specific focus is on the Fiber#transfer method.

require 'fiber'

fiber2 = nil

fiber1 = Fiber.new do
  puts "In Fiber 1"                 # 3
  fiber2.transfer                   # 4
end

fiber2 = Fiber.new do
  puts "In Fiber 2"                  # 1
  fiber1.transfer                    # 2
  puts "In Fiber 2 again"            # 5
  Fiber.yield                        # 6
  puts "Fiber 2 resumed"             # 10
end

fiber3 = Fiber.new do
  puts "In Fiber 3"                  # 8
end

fiber2.resume                        # 0
fiber3.resume                        # 7
fiber2.resume                        # 9

I have numbered the lines of code with the expected serial order of execution on the right. Once fiber3.resume returns and I call fiber2.resume, I expect the execution to continue inside fiber2 at the line marked # 10. Instead, I get the following error:

fiber2.rb:24:in `resume': cannot resume transferred Fiber (FiberError)
    from fiber2.rb:24:in `<main>'

That's an error reported from the last line of the listing: fiber2.resume.

like image 589
CppNoob Avatar asked Jul 05 '16 09:07

CppNoob


2 Answers

It seems that the behavior has changed since Ruby 1.9. While in 1.9, things work the way the question asker assumes, later versions of Ruby changed how #transfer works. I'm testing on 2.4, but this may hold true for earlier versions in the 2.* series.

In 1.9, #transfer could be used for jumping back-and-forth between fibers. It is possible that at that time, #resume could not be used for this purpose. Anyway, in Ruby 2.4 you can use #resume to jump from one fiber into another, and then simply use Fiber.yield() to jump back to the caller.

Example (based on code from the question):

require 'fiber'

fiber2 = nil

fiber1 = Fiber.new do
  puts "In Fiber 1"                 # 3
  Fiber.yield                       # 4 (returns to fiber2)
end

fiber2 = Fiber.new do
  puts "In Fiber 2"                  # 1
  fiber1.resume                      # 2
  puts "In Fiber 2 again"            # 5
  Fiber.yield                        # 6 (returns to main)
  puts "Fiber 2 resumed"             # 10
end

fiber3 = Fiber.new do
  puts "In Fiber 3"                  # 8
end

fiber2.resume                        # 0
fiber3.resume                        # 7
fiber2.resume                        # 9

The use case for #transfer now appears to be when you have two fibers (let's call them A and B) and want to go from A to B, and you don't plan on coming back to A before B finishes. However, Ruby doesn't have a notion of tail call optimization, so A still has to wait around for B to finish up and yield it's final value. Nevertheless, #transfer is essentially now a one-way-ticket.

like image 104
RavensKrag Avatar answered Nov 03 '22 04:11

RavensKrag


You might have found a bug in ruby. When you look at the source code, it is implemented the way you describe it:

https://fossies.org/linux/misc/ruby-2.3.1.tar.gz/ruby-2.3.1/cont.c

Follow the transferred flag, it is set to 1 when you transfer the fiber but it is never reset.

IMO it should be reset when the fiber gain control or when yield is called.

like image 20
Thomas Avatar answered Nov 03 '22 03:11

Thomas