CrackedRuby logo

CrackedRuby

Fiber Resume and Yield

A guide to Ruby's Fiber resume and yield mechanisms for cooperative multitasking and flow control.

Concurrency and Parallelism Fibers
6.3.3

Overview

Ruby implements cooperative multitasking through fibers, lightweight execution units that can pause and resume execution at specific points. The Fiber.yield and Fiber#resume methods form the core communication mechanism between fibers and their callers, creating a cooperative scheduling system where control transfer happens explicitly rather than preemptively.

A fiber represents a suspended execution context that maintains its local variables, call stack, and program counter between yield and resume operations. Unlike threads, fibers run cooperatively within a single thread, transferring control only when explicitly requested through yield operations.

The fiber lifecycle follows a predictable pattern: creation in a suspended state, activation through resume, execution until yield or completion, and potential reactivation through subsequent resume calls. Each fiber maintains its execution state independently, creating isolated contexts for complex control flow patterns.

fiber = Fiber.new do |initial_value|
  puts "Fiber started with: #{initial_value}"
  result = Fiber.yield("first yield")
  puts "Resumed with: #{result}"
  "final return"
end

puts fiber.resume("hello")  # Fiber started, prints "first yield"
puts fiber.resume("world")  # Resumed with: world, prints "final return"

Ruby's fiber implementation supports bidirectional data transfer, where resume can pass values into the fiber and yield can pass values back to the caller. This creates a communication channel that persists across suspension points.

counter_fiber = Fiber.new do
  count = 0
  loop do
    increment = Fiber.yield(count)
    count += increment || 1
  end
end

puts counter_fiber.resume     # => 0
puts counter_fiber.resume(5)  # => 5
puts counter_fiber.resume     # => 6

The fiber scheduling model operates on a call-return basis rather than time-slicing. A fiber continues execution until it explicitly yields control, encounters an exception, or completes its execution block. This deterministic behavior makes fibers suitable for implementing generators, coroutines, and custom iteration patterns.

Basic Usage

Creating a fiber requires a block that defines the fiber's execution context. The fiber begins in a suspended state and requires an initial resume call to start execution. The first resume call activates the fiber and can pass initial parameters to the execution block.

simple_fiber = Fiber.new do |name|
  puts "Hello, #{name}!"
  Fiber.yield "paused"
  puts "Goodbye, #{name}!"
  "completed"
end

result1 = simple_fiber.resume("Alice")  # Prints "Hello, Alice!", returns "paused"
result2 = simple_fiber.resume          # Prints "Goodbye, Alice!", returns "completed"

The Fiber.yield method suspends the current fiber's execution and returns control to the caller. The value passed to yield becomes the return value of the corresponding resume call. When the fiber is resumed, the yield expression evaluates to the value passed to resume.

data_processor = Fiber.new do
  loop do
    input = Fiber.yield "ready"
    processed = input.upcase if input
    Fiber.yield "processed: #{processed}" if processed
  end
end

puts data_processor.resume              # => "ready"
puts data_processor.resume("hello")     # => "processed: HELLO"
puts data_processor.resume              # => "ready"

Fiber state management follows strict rules. A fiber can be in one of several states: created but not started, running, suspended, or terminated. Attempting to resume a terminated fiber raises a FiberError. The Fiber#alive? method indicates whether a fiber can be resumed.

status_fiber = Fiber.new { "done" }

puts status_fiber.alive?   # => true
result = status_fiber.resume
puts result                # => "done"
puts status_fiber.alive?   # => false

begin
  status_fiber.resume
rescue FiberError => e
  puts e.message  # => "dead fiber called"
end

Nested fiber creation creates independent execution contexts. Each fiber maintains its own call stack and can create additional fibers without affecting its parent's execution state. However, fiber-local variables and binding contexts remain isolated between different fibers.

outer_fiber = Fiber.new do
  puts "Outer fiber start"
  
  inner_fiber = Fiber.new do
    puts "Inner fiber executing"
    Fiber.yield "inner result"
    "inner complete"
  end
  
  inner_result = inner_fiber.resume
  Fiber.yield "outer with: #{inner_result}"
  inner_fiber.resume
end

puts outer_fiber.resume  # Prints both, returns "outer with: inner result"
puts outer_fiber.resume  # Returns "inner complete"

Thread Safety & Concurrency

Fibers operate within the context of a single Ruby thread and do not provide thread-safe execution by default. Multiple threads can each maintain their own set of fibers, but individual fibers cannot be safely shared or transferred between threads. The current fiber context is thread-local, meaning Fiber.current returns different values in different threads.

require 'thread'

fiber_storage = {}
mutex = Mutex.new

thread1 = Thread.new do
  fiber = Fiber.new do
    mutex.synchronize { fiber_storage[:thread1] = Fiber.current }
    Fiber.yield "thread1 fiber"
    "thread1 complete"
  end
  fiber.resume
end

thread2 = Thread.new do
  fiber = Fiber.new do
    mutex.synchronize { fiber_storage[:thread2] = Fiber.current }
    Fiber.yield "thread2 fiber"
    "thread2 complete"
  end
  fiber.resume
end

thread1.join
thread2.join

puts fiber_storage[:thread1] != fiber_storage[:thread2]  # => true

When fibers interact with shared mutable state, explicit synchronization becomes necessary. Since fibers yield control voluntarily, race conditions can occur at yield points if multiple threads are involved. Synchronization primitives like mutexes must protect critical sections that span multiple fiber executions.

shared_counter = 0
counter_mutex = Mutex.new

increment_fiber = Fiber.new do |steps|
  steps.times do |i|
    counter_mutex.synchronize do
      current = shared_counter
      Fiber.yield "step #{i}: #{current}"
      shared_counter = current + 1
    end
  end
  "final count: #{shared_counter}"
end

puts increment_fiber.resume(3)  # => "step 0: 0"
puts increment_fiber.resume     # => "step 1: 1"
puts increment_fiber.resume     # => "step 2: 2"
puts increment_fiber.resume     # => "final count: 3"

Fiber scheduling in concurrent environments requires careful coordination. Since fibers do not preempt each other, a fiber that never yields can block other fibers indefinitely. This behavior becomes problematic when integrating fibers with external I/O operations or time-sensitive processing.

class FiberScheduler
  def initialize
    @fibers = []
    @mutex = Mutex.new
  end
  
  def add_fiber(&block)
    fiber = Fiber.new(&block)
    @mutex.synchronize { @fibers << fiber }
    fiber
  end
  
  def run_all
    active_fibers = @mutex.synchronize { @fibers.select(&:alive?) }
    
    while active_fibers.any?
      active_fibers.each do |fiber|
        begin
          result = fiber.resume
          puts "Fiber yielded: #{result}" if result
        rescue FiberError
          # Fiber terminated
        end
      end
      active_fibers = @mutex.synchronize { @fibers.select(&:alive?) }
    end
  end
end

Deadlock scenarios can emerge when fibers wait for resources held by other fibers or threads. Since fiber scheduling is cooperative, detection and resolution of deadlocks requires explicit timeout mechanisms or careful ordering of resource acquisition.

resource_a = Mutex.new
resource_b = Mutex.new

# Potential deadlock scenario
fiber1 = Fiber.new do
  resource_a.synchronize do
    Fiber.yield "fiber1 has resource_a"
    resource_b.synchronize do
      "fiber1 has both resources"
    end
  end
end

fiber2 = Fiber.new do
  resource_b.synchronize do
    Fiber.yield "fiber2 has resource_b"
    resource_a.synchronize do
      "fiber2 has both resources"
    end
  end
end

Advanced Usage

Fiber-based generators provide a clean abstraction for creating custom iteration patterns. By encapsulating complex iteration logic within a fiber, generators can maintain state between iterations while presenting a simple interface to consumers. The generator pattern becomes particularly valuable for processing large datasets or implementing lazy evaluation strategies.

class FibonacciGenerator
  def initialize(limit = Float::INFINITY)
    @limit = limit
    @fiber = create_fiber
  end
  
  def next
    @fiber.resume
  end
  
  def reset
    @fiber = create_fiber
  end
  
  private
  
  def create_fiber
    Fiber.new do
      a, b = 0, 1
      count = 0
      
      while count < @limit
        Fiber.yield a
        a, b = b, a + b
        count += 1
      end
      
      nil  # End of sequence
    end
  end
end

fib = FibonacciGenerator.new(10)
10.times { puts fib.next }

Coroutine patterns enable bidirectional communication between fibers, creating producer-consumer relationships where both parties can send and receive data. This pattern proves effective for implementing pipelines, stream processors, and interactive command processors.

class Pipeline
  def initialize(*stages)
    @stages = stages.map { |stage| create_stage(stage) }
  end
  
  def process(input)
    @stages.inject(input) do |data, stage_fiber|
      stage_fiber.resume(data)
    end
  end
  
  private
  
  def create_stage(processor)
    Fiber.new do
      loop do
        input = Fiber.yield
        output = processor.call(input)
        Fiber.yield output
      end
    end.tap(&:resume)  # Prime the fiber
  end
end

upcase_stage = ->(text) { text.upcase }
reverse_stage = ->(text) { text.reverse }
exclaim_stage = ->(text) { "#{text}!!!" }

pipeline = Pipeline.new(upcase_stage, reverse_stage, exclaim_stage)
result = pipeline.process("hello world")
puts result  # => "DLROW OLLEH!!!"

Fiber composition allows building complex control flow patterns by combining multiple fibers in hierarchical or parallel arrangements. Parent fibers can manage child fiber lifecycles, implementing supervision patterns common in actor-based systems.

class FiberSupervisor
  def initialize
    @children = []
    @results = {}
  end
  
  def spawn(name, &block)
    fiber = Fiber.new do
      begin
        result = yield
        @results[name] = { success: true, value: result }
      rescue => e
        @results[name] = { success: false, error: e.message }
      end
      Fiber.yield :completed
    end
    
    @children << { name: name, fiber: fiber }
    fiber
  end
  
  def supervise
    Fiber.new do
      active = @children.select { |child| child[:fiber].alive? }
      
      while active.any?
        active.each do |child|
          begin
            status = child[:fiber].resume
            if status == :completed
              Fiber.yield "#{child[:name]} completed"
            end
          rescue FiberError
            # Child terminated
          end
        end
        active = @children.select { |child| child[:fiber].alive? }
      end
      
      @results
    end
  end
end

State machines implemented with fibers provide clean separation between states and transitions. Each state can be represented as a fiber section, with yields marking transition points. This approach creates readable, maintainable state machine implementations.

class StateMachine
  def initialize
    @state = :initial
    @fiber = create_machine
  end
  
  def transition(event)
    @fiber.resume(event)
  end
  
  def current_state
    @state
  end
  
  private
  
  def create_machine
    Fiber.new do
      @state = :waiting
      
      loop do
        event = Fiber.yield @state
        
        case [@state, event]
        when [:waiting, :start]
          @state = :running
        when [:running, :pause]
          @state = :paused
        when [:paused, :resume]
          @state = :running
        when [:running, :stop], [:paused, :stop]
          @state = :stopped
          break
        end
      end
      
      @state
    end
  end
end

Performance & Memory

Fiber creation overhead is significantly lower than thread creation, making fibers suitable for scenarios requiring many lightweight execution contexts. However, each fiber maintains its own stack space, typically consuming several kilobytes of memory per fiber. Memory usage scales linearly with the number of active fibers.

require 'benchmark'

def measure_memory
  GC.start
  GC.disable
  yield
ensure
  GC.enable
end

# Memory usage comparison
fiber_count = 1000

memory_before = GC.stat[:total_allocated_objects]
fibers = Array.new(fiber_count) do |i|
  Fiber.new { Fiber.yield "fiber #{i}" }
end
memory_after = GC.stat[:total_allocated_objects]

puts "Memory per fiber: #{(memory_after - memory_before) / fiber_count} objects"

# Execution time comparison
Benchmark.bm(10) do |x|
  x.report("fiber create") do
    1000.times { Fiber.new { :done } }
  end
  
  x.report("fiber resume") do
    fiber = Fiber.new { loop { Fiber.yield :result } }
    1000.times { fiber.resume }
  end
end

Context switching between fibers involves saving and restoring the execution context, including local variables and call stack. This operation is faster than thread context switching but still carries computational cost proportional to the stack depth and local variable count.

class PerformanceProfiler
  def self.profile_fiber_switching(iterations)
    fiber = Fiber.new do
      iterations.times do |i|
        start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        Fiber.yield start_time
      end
    end
    
    switch_times = []
    iterations.times do
      resume_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      yield_time = fiber.resume
      resume_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      
      switch_times << (resume_end - resume_start) * 1_000_000  # microseconds
    end
    
    {
      mean: switch_times.sum / switch_times.size,
      min: switch_times.min,
      max: switch_times.max
    }
  end
end

stats = PerformanceProfiler.profile_fiber_switching(10_000)
puts "Average switch time: #{stats[:mean]:.2f}μs"

Stack overflow can occur in deeply nested fiber execution, particularly when fibers create additional fibers recursively. Ruby's stack size limits apply to the combined call stack of all fiber contexts within a thread. Monitoring stack depth becomes critical for preventing overflow conditions.

class StackMonitor
  def self.measure_stack_depth(fiber)
    depth = 0
    
    monitor_fiber = Fiber.new do
      begin
        catch(:stack_check) do
          check_depth = proc do |level|
            if level > 1000  # Arbitrary deep limit
              throw :stack_check, level
            end
            check_depth.call(level + 1)
          end
          check_depth.call(0)
        end
      rescue SystemStackError
        :stack_overflow
      end
    end
    
    monitor_fiber.resume
  end
  
  def self.safe_fiber_nesting(max_depth = 100)
    current_depth = 0
    
    create_nested = proc do |level|
      return :max_depth_reached if level >= max_depth
      
      nested_fiber = Fiber.new do
        current_depth = level
        Fiber.yield level
        create_nested.call(level + 1)
      end
      
      nested_fiber.resume
      nested_fiber.resume if nested_fiber.alive?
    end
    
    create_nested.call(0)
  end
end

Memory leaks can develop when fibers hold references to large objects or when circular references prevent garbage collection. Fiber-local storage and instance variables within fiber blocks persist until the fiber terminates, potentially retaining objects longer than expected.

class FiberMemoryManager
  def self.create_managed_fiber(&block)
    fiber = Fiber.new do
      result = nil
      begin
        result = yield
      ensure
        # Explicit cleanup of large objects
        ObjectSpace.garbage_collect
      end
      result
    end
    
    # Weak reference to prevent retention
    ObjectSpace.define_finalizer(fiber) do |id|
      puts "Fiber #{id} being finalized"
    end
    
    fiber
  end
  
  def self.monitor_fiber_memory(fiber)
    initial_objects = ObjectSpace.count_objects[:TOTAL]
    
    yield fiber if block_given?
    
    final_objects = ObjectSpace.count_objects[:TOTAL]
    final_objects - initial_objects
  end
end

Common Pitfalls

Fiber execution order depends on the sequence of resume calls rather than creation order. Developers often assume fibers execute in creation order, leading to unexpected behavior when multiple fibers require coordinated execution. The scheduling remains entirely explicit, requiring careful orchestration for complex workflows.

# Incorrect assumption: fibers execute in creation order
fibers = []
3.times do |i|
  fibers << Fiber.new { puts "Fiber #{i} executing" }
end

# This executes in reverse order
fibers.reverse.each(&:resume)
# Output:
# Fiber 2 executing
# Fiber 1 executing  
# Fiber 0 executing

# Correct approach: explicit ordering
execution_queue = [0, 1, 2]
execution_queue.each { |i| fibers[i].resume }

Exception handling across fiber boundaries requires special consideration. Exceptions raised within a fiber propagate to the caller's resume call, but exceptions in the calling context do not automatically terminate active fibers. This asymmetric behavior can lead to resource leaks and inconsistent state.

# Exception propagation gotcha
fiber = Fiber.new do
  begin
    Fiber.yield "step 1"
    raise StandardError, "fiber error"
  rescue => e
    puts "Caught in fiber: #{e.message}"
    Fiber.yield "step 2"  
  end
end

fiber.resume  # => "step 1"

begin
  fiber.resume  # Raises StandardError
rescue => e
  puts "Caught in caller: #{e.message}"
  # Fiber state remains alive but may be inconsistent
  puts fiber.alive?  # => true, but potentially corrupted
end

Variable scope confusion arises when fibers access variables from their creation context versus their execution context. Local variables created within the fiber block remain fiber-local, while variables captured from the surrounding scope are shared with the creation context.

# Scope confusion example
outer_var = "original"

fiber = Fiber.new do
  outer_var = "modified in fiber"
  inner_var = "fiber local"
  Fiber.yield [outer_var, inner_var]
  outer_var = "modified again"
  [outer_var, inner_var]
end

puts outer_var  # => "original"
result1 = fiber.resume
puts result1    # => ["modified in fiber", "fiber local"]
puts outer_var  # => "modified in fiber" (shared reference)

result2 = fiber.resume
puts result2    # => ["modified again", "fiber local"]
puts outer_var  # => "modified again"

Nested yield operations create complex control flow that can be difficult to reason about. When a fiber yields to another fiber that subsequently yields, the return path becomes convoluted, making debugging and maintenance challenging.

# Nested yield complexity
primary = Fiber.new do
  puts "Primary start"
  
  secondary = Fiber.new do
    puts "Secondary start"
    Fiber.yield "secondary yield"
    puts "Secondary resume"
    "secondary end"
  end
  
  result = secondary.resume
  Fiber.yield "primary yield: #{result}"
  final = secondary.resume
  "primary end: #{final}"
end

puts primary.resume     # Prints start messages, returns "primary yield: secondary yield"
puts primary.resume     # Prints resume message, returns "primary end: secondary end"

Resource cleanup becomes problematic when fibers terminate unexpectedly or when references to external resources are not properly managed. Unlike threads, fibers do not have automatic cleanup mechanisms, requiring explicit resource management.

# Resource cleanup pattern
class ResourceManager
  def self.with_managed_fiber(resources = {})
    fiber = Fiber.new do
      begin
        yield resources
      ensure
        cleanup_resources(resources)
      end
    end
    
    begin
      result = nil
      while fiber.alive?
        result = fiber.resume
      end
      result
    rescue => e
      # Force cleanup on exception
      cleanup_resources(resources)
      raise
    end
  end
  
  private
  
  def self.cleanup_resources(resources)
    resources.each do |name, resource|
      resource.close if resource.respond_to?(:close)
      resource.cleanup if resource.respond_to?(:cleanup)
    end
  end
end

# Usage
ResourceManager.with_managed_fiber(file: File.open('data.txt')) do |res|
  Fiber.yield "processing #{res[:file].read}"
  "completed"
end

Reference

Core Methods

Method Parameters Returns Description
Fiber.new(&block) block (Proc) Fiber Creates new fiber with execution block
Fiber.yield(*args) *args (Objects) Object Suspends current fiber, returns args to caller
#resume(*args) *args (Objects) Object Resumes fiber execution with optional arguments
#alive? None Boolean Returns true if fiber can be resumed
Fiber.current None Fiber Returns currently executing fiber

Fiber States

State Description Methods Available
Created Initial state after Fiber.new #resume, #alive?
Running Currently executing Fiber.yield (from within)
Suspended Paused at yield point #resume, #alive?
Terminated Completed execution #alive? (returns false)

Exception Types

Exception Raised When Recovery
FiberError Resuming dead fiber Check #alive? before resume
FiberError Yielding from root fiber Only yield from within fiber block
SystemStackError Stack overflow in fiber Reduce recursion depth

Common Patterns

Pattern Use Case Implementation
Generator Lazy iteration Yield values in loop, resume for next
Producer-Consumer Data pipeline Bidirectional yield/resume communication
State Machine Controlled transitions Yield on state changes, resume with events
Coroutine Cooperative multitasking Multiple fibers with explicit scheduling

Performance Characteristics

Metric Typical Value Notes
Creation time ~1-5μs Much faster than thread creation
Resume/yield time ~0.1-1μs Depends on stack depth
Memory per fiber ~4-8KB Stack space allocation
Maximum fibers ~100K-1M Limited by available memory

Debugging Methods

Method Purpose Example Usage
Fiber.current Identify active fiber Log current fiber in debug output
#inspect Fiber object details Display fiber state information
caller Call stack trace Debug yield/resume call locations
ObjectSpace.count_objects Memory tracking Monitor fiber memory consumption

Integration Considerations

Context Behavior Recommendation
Threads Thread-local fiber context Create fibers within thread boundaries
Exceptions Propagate to resume caller Handle exceptions at resume points
Garbage Collection Fibers prevent collection Ensure fiber completion for cleanup
Signal Handlers May interrupt fiber execution Use signal-safe fiber patterns