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
.
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.