5

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.

CppNoob
  • 2,322
  • 1
  • 24
  • 35

2 Answers2

4

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.

RavensKrag
  • 895
  • 1
  • 7
  • 9
2

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.

Thomas
  • 1,613
  • 8
  • 8