# # Fibers are primitives for implementing light weight cooperative concurrency in # Ruby. Basically they are a means of creating code blocks that can be paused # and resumed, much like threads. The main difference is that they are never # preempted and that the scheduling must be done by the programmer and not the # VM. # # As opposed to other stackless light weight concurrency models, each fiber # comes with a stack. This enables the fiber to be paused from deeply nested # function calls within the fiber block. See the ruby(1) manpage to configure # the size of the fiber stack(s). # # When a fiber is created it will not run automatically. Rather it must be # explicitly asked to run using the Fiber#resume method. The code running inside # the fiber can give up control by calling Fiber.yield in which case it yields # control back to caller (the caller of the Fiber#resume). # # Upon yielding or termination the Fiber returns the value of the last executed # expression # # For instance: # # fiber = Fiber.new do # Fiber.yield 1 # 2 # end # # puts fiber.resume # puts fiber.resume # puts fiber.resume # # *produces* # # 1 # 2 # FiberError: dead fiber called # # The Fiber#resume method accepts an arbitrary number of parameters, if it is # the first call to #resume then they will be passed as block arguments. # Otherwise they will be the return value of the call to Fiber.yield # # Example: # # fiber = Fiber.new do |first| # second = Fiber.yield first + 2 # end # # puts fiber.resume 10 # puts fiber.resume 1_000_000 # puts fiber.resume "The fiber will be dead before I can cause trouble" # # *produces* # # 12 # 1000000 # FiberError: dead fiber called # # ## Non-blocking Fibers # # The concept of *non-blocking fiber* was introduced in Ruby 3.0. A non-blocking # fiber, when reaching a operation that would normally block the fiber (like # `sleep`, or wait for another process or I/O) will yield control to other # fibers and allow the *scheduler* to handle blocking and waking up (resuming) # this fiber when it can proceed. # # For a Fiber to behave as non-blocking, it need to be created in Fiber.new with # `blocking: false` (which is the default), and Fiber.scheduler should be set # with Fiber.set_scheduler. If Fiber.scheduler is not set in the current thread, # blocking and non-blocking fibers' behavior is identical. # # Ruby doesn't provide a scheduler class: it is expected to be implemented by # the user and correspond to Fiber::Scheduler. # # There is also Fiber.schedule method, which is expected to immediately perform # the given block in a non-blocking manner. Its actual implementation is up to # the scheduler. # class Fiber < Object # # Returns the value of the fiber storage variable identified by `key`. # # The `key` must be a symbol, and the value is set by Fiber#[]= or Fiber#store. # # See also Fiber::[]=. # def self.[]: (Symbol) -> untyped # # Assign `value` to the fiber storage variable identified by `key`. The variable # is created if it doesn't exist. # # `key` must be a Symbol, otherwise a TypeError is raised. # # See also Fiber::[]. # def self.[]=: [A] (Symbol, A) -> A # # Returns `false` if the current fiber is non-blocking. Fiber is non-blocking if # it was created via passing `blocking: false` to Fiber.new, or via # Fiber.schedule. # # If the current Fiber is blocking, the method returns 1. Future developments # may allow for situations where larger integers could be returned. # # Note that, even if the method returns `false`, Fiber behaves differently only # if Fiber.scheduler is set in the current thread. # # See the "Non-blocking fibers" section in class docs for details. # def self.blocking?: () -> untyped # # Returns the current fiber. If you are not running in the context of a fiber # this method will return the root fiber. # def self.current: () -> Fiber # # Returns the Fiber scheduler, that was last set for the current thread with # Fiber.set_scheduler if and only if the current fiber is non-blocking. # def self.current_scheduler: () -> untyped? # # The method is *expected* to immediately run the provided block of code in a # separate non-blocking fiber. # # puts "Go to sleep!" # # Fiber.set_scheduler(MyScheduler.new) # # Fiber.schedule do # puts "Going to sleep" # sleep(1) # puts "I slept well" # end # # puts "Wakey-wakey, sleepyhead" # # Assuming MyScheduler is properly implemented, this program will produce: # # Go to sleep! # Going to sleep # Wakey-wakey, sleepyhead # ...1 sec pause here... # I slept well # # ...e.g. on the first blocking operation inside the Fiber (`sleep(1)`), the # control is yielded to the outside code (main fiber), and *at the end of that # execution*, the scheduler takes care of properly resuming all the blocked # fibers. # # Note that the behavior described above is how the method is *expected* to # behave, actual behavior is up to the current scheduler's implementation of # Fiber::Scheduler#fiber method. Ruby doesn't enforce this method to behave in # any particular way. # # If the scheduler is not set, the method raises `RuntimeError (No scheduler is # available!)`. # def self.schedule: () { () -> void } -> Fiber # # Returns the Fiber scheduler, that was last set for the current thread with # Fiber.set_scheduler. Returns `nil` if no scheduler is set (which is the # default), and non-blocking fibers' behavior is the same as blocking. (see # "Non-blocking fibers" section in class docs for details about the scheduler # concept). # def self.scheduler: () -> untyped? # # Sets the Fiber scheduler for the current thread. If the scheduler is set, # non-blocking fibers (created by Fiber.new with `blocking: false`, or by # Fiber.schedule) call that scheduler's hook methods on potentially blocking # operations, and the current thread will call scheduler's `close` method on # finalization (allowing the scheduler to properly manage all non-finished # fibers). # # `scheduler` can be an object of any class corresponding to Fiber::Scheduler. # Its implementation is up to the user. # # See also the "Non-blocking fibers" section in class docs. # def self.set_scheduler: (untyped) -> untyped # # Yields control back to the context that resumed the fiber, passing along any # arguments that were passed to it. The fiber will resume processing at this # point when #resume is called next. Any arguments passed to the next #resume # will be the value that this Fiber.yield expression evaluates to. # def self.yield: (*untyped) -> untyped # # Creates new Fiber. Initially, the fiber is not running and can be resumed with # #resume. Arguments to the first #resume call will be passed to the block: # # f = Fiber.new do |initial| # current = initial # loop do # puts "current: #{current.inspect}" # current = Fiber.yield # end # end # f.resume(100) # prints: current: 100 # f.resume(1, 2, 3) # prints: current: [1, 2, 3] # f.resume # prints: current: nil # # ... and so on ... # # If `blocking: false` is passed to `Fiber.new`, *and* current thread has a # Fiber.scheduler defined, the Fiber becomes non-blocking (see "Non-blocking # Fibers" section in class docs). # # If the `storage` is unspecified, the default is to inherit a copy of the # storage from the current fiber. This is the same as specifying `storage: # true`. # # Fiber[:x] = 1 # Fiber.new do # Fiber[:x] # => 1 # Fiber[:x] = 2 # end.resume # Fiber[:x] # => 1 # # If the given `storage` is `nil`, this function will lazy initialize the # internal storage, which starts as an empty hash. # # Fiber[:x] = "Hello World" # Fiber.new(storage: nil) do # Fiber[:x] # nil # end # # Otherwise, the given `storage` is used as the new fiber's storage, and it must # be an instance of Hash. # # Explicitly using `storage: true` is currently experimental and may change in # the future. # def initialize: (?blocking: boolish, ?storage: true | Hash[Symbol | String, untyped] | nil) { (*untyped) -> void } -> void # # Returns true if the fiber can still be resumed (or transferred to). After # finishing execution of the fiber block this method will always return `false`. # def alive?: () -> bool # # Returns the current execution stack of the fiber. `start`, `count` and `end` # allow to select only parts of the backtrace. # # def level3 # Fiber.yield # end # # def level2 # level3 # end # # def level1 # level2 # end # # f = Fiber.new { level1 } # # # It is empty before the fiber started # f.backtrace # #=> [] # # f.resume # # f.backtrace # #=> ["test.rb:2:in `yield'", "test.rb:2:in `level3'", "test.rb:6:in `level2'", "test.rb:10:in `level1'", "test.rb:13:in `block in
'"] # p f.backtrace(1) # start from the item 1 # #=> ["test.rb:2:in `level3'", "test.rb:6:in `level2'", "test.rb:10:in `level1'", "test.rb:13:in `block in
'"] # p f.backtrace(2, 2) # start from item 2, take 2 # #=> ["test.rb:6:in `level2'", "test.rb:10:in `level1'"] # p f.backtrace(1..3) # take items from 1 to 3 # #=> ["test.rb:2:in `level3'", "test.rb:6:in `level2'", "test.rb:10:in `level1'"] # # f.resume # # # It is nil after the fiber is finished # f.backtrace # #=> nil # def backtrace: (?Integer start, ?Integer count) -> Array[String]? | (Range[Integer]) -> Array[String]? # # Like #backtrace, but returns each line of the execution stack as a # Thread::Backtrace::Location. Accepts the same arguments as #backtrace. # # f = Fiber.new { Fiber.yield } # f.resume # loc = f.backtrace_locations.first # loc.label #=> "yield" # loc.path #=> "test.rb" # loc.lineno #=> 1 # def backtrace_locations: (?Integer start, ?Integer count) -> Array[Thread::Backtrace::Location]? | (Range[Integer]) -> Array[Thread::Backtrace::Location]? # # Returns `true` if `fiber` is blocking and `false` otherwise. Fiber is # non-blocking if it was created via passing `blocking: false` to Fiber.new, or # via Fiber.schedule. # # Note that, even if the method returns `false`, the fiber behaves differently # only if Fiber.scheduler is set in the current thread. # # See the "Non-blocking fibers" section in class docs for details. # def blocking?: () -> bool # # alias inspect to_s # # Raises an exception in the fiber at the point at which the last `Fiber.yield` # was called. If the fiber has not been started or has already run to # completion, raises `FiberError`. If the fiber is yielding, it is resumed. If # it is transferring, it is transferred into. But if it is resuming, raises # `FiberError`. # # With no arguments, raises a `RuntimeError`. With a single `String` argument, # raises a `RuntimeError` with the string as a message. Otherwise, the first # parameter should be the name of an `Exception` class (or an object that # returns an `Exception` object when sent an `exception` message). The optional # second parameter sets the message associated with the exception, and the third # parameter is an array of callback information. Exceptions are caught by the # `rescue` clause of `begin...end` blocks. # def raise: (?string msg) -> untyped | (_Exception, ?string msg, ?Array[string] backtrace) -> untyped # # Resumes the fiber from the point at which the last Fiber.yield was called, or # starts running it if it is the first call to #resume. Arguments passed to # resume will be the value of the Fiber.yield expression or will be passed as # block parameters to the fiber's block if this is the first #resume. # # Alternatively, when resume is called it evaluates to the arguments passed to # the next Fiber.yield statement inside the fiber's block or to the block value # if it runs to completion without any Fiber.yield # def resume: (*untyped) -> untyped # # Returns a copy of the storage hash for the fiber. The method can only be # called on the Fiber.current. # def storage: () -> Hash[Symbol | String, untyped] # # Sets the storage hash for the fiber. This feature is experimental and may # change in the future. The method can only be called on the Fiber.current. # # You should be careful about using this method as you may inadvertently clear # important fiber-storage state. You should mostly prefer to assign specific # keys in the storage using Fiber::[]=. # # You can also use `Fiber.new(storage: nil)` to create a fiber with an empty # storage. # # Example: # # while request = request_queue.pop # # Reset the per-request state: # Fiber.current.storage = nil # handle_request(request) # end # def storage=: (Hash[Symbol | String, untyped]) -> Hash[Symbol | String, untyped] # # def to_s: () -> untyped # # Transfer control to another fiber, resuming it from where it last stopped or # starting it if it was not resumed before. The calling fiber will be suspended # much like in a call to Fiber.yield. # # The fiber which receives the transfer call treats it much like a resume call. # Arguments passed to transfer are treated like those passed to resume. # # The two style of control passing to and from fiber (one is #resume and # Fiber::yield, another is #transfer to and from fiber) can't be freely mixed. # # * If the Fiber's lifecycle had started with transfer, it will never be able # to yield or be resumed control passing, only finish or transfer back. (It # still can resume other fibers that are allowed to be resumed.) # * If the Fiber's lifecycle had started with resume, it can yield or transfer # to another Fiber, but can receive control back only the way compatible # with the way it was given away: if it had transferred, it only can be # transferred back, and if it had yielded, it only can be resumed back. # After that, it again can transfer or yield. # # # If those rules are broken FiberError is raised. # # For an individual Fiber design, yield/resume is easier to use (the Fiber just # gives away control, it doesn't need to think about who the control is given # to), while transfer is more flexible for complex cases, allowing to build # arbitrary graphs of Fibers dependent on each other. # # Example: # # manager = nil # For local var to be visible inside worker block # # # This fiber would be started with transfer # # It can't yield, and can't be resumed # worker = Fiber.new { |work| # puts "Worker: starts" # puts "Worker: Performed #{work.inspect}, transferring back" # # Fiber.yield # this would raise FiberError: attempt to yield on a not resumed fiber # # manager.resume # this would raise FiberError: attempt to resume a resumed fiber (double resume) # manager.transfer(work.capitalize) # } # # # This fiber would be started with resume # # It can yield or transfer, and can be transferred # # back or resumed # manager = Fiber.new { # puts "Manager: starts" # puts "Manager: transferring 'something' to worker" # result = worker.transfer('something') # puts "Manager: worker returned #{result.inspect}" # # worker.resume # this would raise FiberError: attempt to resume a transferring fiber # Fiber.yield # this is OK, the fiber transferred from and to, now it can yield # puts "Manager: finished" # } # # puts "Starting the manager" # manager.resume # puts "Resuming the manager" # # manager.transfer # this would raise FiberError: attempt to transfer to a yielding fiber # manager.resume # # *produces* # # Starting the manager # Manager: starts # Manager: transferring 'something' to worker # Worker: starts # Worker: Performed "something", transferring back # Manager: worker returned "Something" # Resuming the manager # Manager: finished # def transfer: (*untyped) -> untyped end