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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With