CrackedRuby logo

CrackedRuby

Fiber Creation

Creating and managing fibers for cooperative concurrency in Ruby applications.

Concurrency and Parallelism Fibers
6.3.1

Overview

Fibers in Ruby provide cooperative multitasking through user-controlled context switching. Unlike threads, fibers yield execution control explicitly rather than preemptively. The Fiber class creates lightweight execution contexts that pause and resume at specified points.

Ruby implements fibers as objects that encapsulate execution state. Each fiber maintains its own stack and can suspend execution using Fiber.yield, returning control to the caller. The caller can then resume the fiber using #resume, passing values between contexts.

# Basic fiber creation and execution
fiber = Fiber.new do
  puts "Fiber started"
  Fiber.yield "paused"
  puts "Fiber resumed"
  "completed"
end

result1 = fiber.resume  # => "paused"
result2 = fiber.resume  # => "completed"

Fibers operate in three states: suspended, running, and terminated. New fibers begin in suspended state. Calling #resume transitions them to running. When a fiber completes or yields, it returns to suspended state. Terminated fibers cannot be resumed.

# Fiber state demonstration
fiber = Fiber.new { |x| x * 2 }

fiber.alive?    # => true (suspended)
result = fiber.resume(5)  # => 10
fiber.alive?    # => false (terminated)

The primary use cases include iterators, generators, asynchronous programming patterns, and pipeline processing. Fibers excel when you need fine-grained control over execution flow without the overhead of full threads.

Basic Usage

Creating fibers requires a block that defines the fiber's execution context. The block receives arguments passed to the first #resume call. Subsequent #resume calls pass arguments to the most recent Fiber.yield.

# Fiber with initialization and yield values
counter = Fiber.new do |start|
  current = start
  loop do
    yielded_value = Fiber.yield current
    current += yielded_value
  end
end

counter.resume(10)    # => 10 (initial value)
counter.resume(5)     # => 15 (10 + 5)
counter.resume(3)     # => 18 (15 + 3)

The Fiber.yield method suspends the current fiber and returns control to the calling context. It accepts an optional value that becomes the return value of #resume. When the fiber is resumed, Fiber.yield returns the value passed to #resume.

# Bidirectional value passing
processor = Fiber.new do
  input = Fiber.yield "ready"
  while input
    result = input.upcase
    input = Fiber.yield result
  end
  "finished"
end

processor.resume         # => "ready"
processor.resume("hello") # => "HELLO"
processor.resume("world") # => "WORLD"
processor.resume(nil)     # => "finished"

Fiber creation supports various patterns for data processing. Generator-style fibers produce sequences of values, while processor fibers transform input data. Pipeline fibers connect multiple processing stages.

# Generator pattern
fibonacci = Fiber.new do
  a, b = 0, 1
  loop do
    Fiber.yield a
    a, b = b, a + b
  end
end

10.times { print "#{fibonacci.resume} " }
# => 0 1 1 2 3 5 8 13 21 34

Nested fiber creation allows complex execution hierarchies. Parent fibers can create and manage child fibers, coordinating their execution and handling their results.

# Nested fiber coordination
parent = Fiber.new do
  child1 = Fiber.new { (1..3).each { |n| Fiber.yield "child1: #{n}" } }
  child2 = Fiber.new { (1..3).each { |n| Fiber.yield "child2: #{n}" } }
  
  while child1.alive? || child2.alive?
    Fiber.yield child1.resume if child1.alive?
    Fiber.yield child2.resume if child2.alive?
  end
end

results = []
while parent.alive?
  result = parent.resume
  results << result if result
end
# Results contain interleaved output from both children

Thread Safety & Concurrency

Fibers provide thread-safe cooperative concurrency within a single thread. Unlike preemptive threads, fibers only yield control at explicit yield points, eliminating many race conditions. However, fiber safety concerns arise when sharing data between fibers or integrating with threaded code.

Each fiber maintains independent local variables and execution context. Sharing mutable objects between fibers requires careful coordination to prevent state corruption during context switches.

# Safe fiber data isolation
shared_data = { counter: 0 }

fiber1 = Fiber.new do
  5.times do
    local_value = shared_data[:counter]
    Fiber.yield "fiber1 read: #{local_value}"
    shared_data[:counter] = local_value + 1
    Fiber.yield "fiber1 wrote: #{shared_data[:counter]}"
  end
end

fiber2 = Fiber.new do
  5.times do
    local_value = shared_data[:counter]
    Fiber.yield "fiber2 read: #{local_value}"
    shared_data[:counter] = local_value + 10
    Fiber.yield "fiber2 wrote: #{shared_data[:counter]}"
  end
end

# Controlled alternating execution prevents corruption
10.times do |i|
  active_fiber = i.even? ? fiber1 : fiber2
  puts active_fiber.resume if active_fiber.alive?
end

When integrating fibers with threads, synchronization becomes critical. Fiber resumption must occur from the same thread that created the fiber. Cross-thread fiber access raises FiberError.

# Thread-fiber integration patterns
require 'thread'

class FiberScheduler
  def initialize
    @fibers = Queue.new
    @results = Queue.new
  end
  
  def schedule(fiber)
    @fibers << fiber
  end
  
  def run
    Thread.new do
      while fiber = @fibers.pop
        if fiber.alive?
          result = fiber.resume
          @results << { fiber: fiber, result: result }
          schedule(fiber) if fiber.alive?
        end
      end
    end
  end
  
  def get_result
    @results.pop
  end
end

scheduler = FiberScheduler.new
worker_thread = scheduler.run

# Schedule fibers from main thread
3.times do |i|
  fiber = Fiber.new do
    5.times { |j| Fiber.yield "fiber #{i} step #{j}" }
  end
  scheduler.schedule(fiber)
end

# Collect results
results = []
15.times { results << scheduler.get_result }

Fiber-local variables provide isolated storage per fiber context. The Fiber[] and Fiber[]= methods access fiber-local storage, similar to thread-local variables but scoped to individual fibers.

# Fiber-local storage
def process_with_context(name, &block)
  Fiber.new do
    Fiber[:name] = name
    Fiber[:start_time] = Time.now
    
    result = block.call
    duration = Time.now - Fiber[:start_time]
    
    { result: result, name: Fiber[:name], duration: duration }
  end
end

fiber1 = process_with_context("task1") do
  Fiber.yield "step1"
  sleep 0.1
  Fiber.yield "step2"
  "completed"
end

fiber2 = process_with_context("task2") do
  Fiber.yield "step1"
  sleep 0.05
  "completed"
end

# Each fiber maintains separate context
puts fiber1.resume  # => "step1"
puts fiber2.resume  # => "step1"
puts fiber1.resume  # => "step2"
puts fiber2.resume  # => {:result=>"completed", :name=>"task2", :duration=>0.05}
puts fiber1.resume  # => {:result=>"completed", :name=>"task1", :duration=>0.1}

Performance & Memory

Fibers provide significant performance advantages over threads for I/O-bound operations. Fiber context switching occurs in user space without kernel involvement, reducing overhead. Memory usage scales better since fibers use smaller stacks than system threads.

Fiber creation allocates approximately 4KB stack space per fiber on most platforms. This contrasts with thread stacks that typically consume 1-2MB. Applications can create thousands of fibers with manageable memory footprint.

# Memory usage comparison
require 'benchmark'

def measure_memory(&block)
  GC.start
  before = GC.stat(:total_allocated_objects)
  yield
  after = GC.stat(:total_allocated_objects)
  after - before
end

# Fiber memory overhead
fiber_objects = measure_memory do
  fibers = 1000.times.map do |i|
    Fiber.new { Fiber.yield i }
  end
  fibers.each(&:resume)
end

puts "Fiber creation allocated #{fiber_objects} objects"

# Thread comparison would show significantly higher allocation

Fiber performance excels in scenarios requiring frequent context switching. Unlike threads, fiber switches don't require system calls or CPU context preservation. This makes fibers ideal for state machines and event-driven processing.

# Performance comparison: fiber vs thread switching
require 'benchmark'

def fiber_switching_test(iterations)
  fiber = Fiber.new do
    iterations.times { Fiber.yield }
  end
  
  iterations.times { fiber.resume }
end

def thread_switching_test(iterations)
  queue1, queue2 = Queue.new, Queue.new
  
  thread = Thread.new do
    iterations.times do
      queue2.push :resume
      queue1.pop
    end
  end
  
  iterations.times do
    queue2.pop
    queue1.push :continue
  end
  
  thread.join
end

Benchmark.bm(10) do |x|
  x.report("fibers:") { fiber_switching_test(10_000) }
  x.report("threads:") { thread_switching_test(10_000) }
end

Fiber garbage collection requires attention to prevent memory leaks. Suspended fibers remain in memory until explicitly terminated or garbage collected. Long-running applications should implement fiber cleanup strategies.

# Fiber lifecycle management
class FiberPool
  def initialize(max_size = 100)
    @fibers = []
    @max_size = max_size
  end
  
  def execute(&block)
    fiber = get_fiber(&block)
    result = fiber.resume
    return_fiber(fiber) if fiber.alive?
    result
  end
  
  private
  
  def get_fiber(&block)
    @fibers.pop || create_fiber(&block)
  end
  
  def create_fiber(&block)
    Fiber.new do
      loop do
        block = Fiber.yield :ready
        break unless block
        block.call
      end
    end
  end
  
  def return_fiber(fiber)
    @fibers.push(fiber) if @fibers.size < @max_size
  end
  
  def cleanup_terminated
    @fibers.reject!(&:alive?)
  end
end

pool = FiberPool.new
results = 1000.times.map do |i|
  pool.execute { "result #{i}" }
end
pool.cleanup_terminated

Stack overflow protection becomes important with deeply nested fiber calls. Ruby provides stack size limits, but recursive fiber patterns can exhaust available stack space.

# Stack-safe recursive fiber pattern
def safe_recursive_fiber(depth, max_depth = 1000)
  Fiber.new do
    if depth > max_depth
      Fiber.yield "max depth reached"
    else
      Fiber.yield depth
      child = safe_recursive_fiber(depth + 1, max_depth)
      child.resume
    end
  end
end

fiber = safe_recursive_fiber(1)
while fiber.alive?
  result = fiber.resume
  puts result
  break if result == "max depth reached"
end

Production Patterns

Production applications use fibers for asynchronous processing, web server implementations, and background job processing. Fiber-based servers handle thousands of concurrent connections without thread overhead.

Event-driven architectures benefit from fiber-based request handling. Each request runs in its own fiber, yielding during I/O operations to allow other requests to process.

# HTTP server with fiber-based request handling
require 'socket'

class FiberHTTPServer
  def initialize(port = 8080)
    @port = port
    @server = TCPServer.new(port)
    @connections = []
  end
  
  def start
    puts "Server started on port #{@port}"
    
    loop do
      # Accept new connections
      if client = @server.accept_nonblock rescue nil
        @connections << create_connection_fiber(client)
      end
      
      # Process existing connections
      @connections.reject! do |fiber|
        if fiber.alive?
          fiber.resume
          !fiber.alive?
        else
          true
        end
      end
      
      sleep 0.001  # Prevent busy waiting
    end
  end
  
  private
  
  def create_connection_fiber(client)
    Fiber.new do
      handle_request(client)
    ensure
      client.close
    end
  end
  
  def handle_request(client)
    request = read_request(client)
    Fiber.yield  # Allow other connections to process
    
    response = process_request(request)
    Fiber.yield  # Yield during response generation
    
    send_response(client, response)
  end
  
  def read_request(client)
    # Read HTTP request headers
    client.gets("\r\n\r\n")
  end
  
  def process_request(request)
    # Process request and generate response
    "HTTP/1.1 200 OK\r\n\r\nHello from Fiber Server!"
  end
  
  def send_response(client, response)
    client.write(response)
  end
end

# server = FiberHTTPServer.new
# server.start

Background job processing with fibers enables efficient task scheduling without thread overhead. Jobs yield between processing steps, allowing fair resource allocation.

# Fiber-based job processing system
class JobProcessor
  def initialize
    @jobs = []
    @running = false
  end
  
  def add_job(&block)
    @jobs << JobFiber.new(&block)
  end
  
  def start
    @running = true
    
    while @running && @jobs.any?(&:alive?)
      @jobs.each do |job|
        if job.alive?
          begin
            job.resume
          rescue => e
            job.handle_error(e)
          end
        end
      end
      
      @jobs.reject! { |job| !job.alive? }
      sleep 0.01
    end
  end
  
  def stop
    @running = false
  end
  
  class JobFiber < Fiber
    def initialize(&block)
      @error_handler = nil
      super(&block)
    end
    
    def handle_error(error)
      puts "Job error: #{error.message}"
    end
  end
end

# Usage example
processor = JobProcessor.new

# Add CPU-intensive jobs that yield periodically
processor.add_job do
  (1..1000).each do |i|
    # Simulate work
    result = i * i
    Fiber.yield if i % 10 == 0  # Yield every 10 iterations
  end
  puts "Job 1 completed"
end

processor.add_job do
  (1..500).each do |i|
    # Simulate different work pattern
    result = Math.sqrt(i)
    Fiber.yield if i % 25 == 0  # Different yield frequency
  end
  puts "Job 2 completed"
end

# processor.start

Database connection pooling with fibers manages database access efficiently. Connections yield during query execution, allowing other operations to proceed.

# Fiber-aware database connection pool
class FiberConnectionPool
  def initialize(size = 5)
    @connections = Array.new(size) { create_connection }
    @waiting_fibers = []
  end
  
  def with_connection
    connection = acquire_connection
    yield connection
  ensure
    release_connection(connection)
  end
  
  private
  
  def acquire_connection
    if connection = @connections.pop
      connection
    else
      # No connections available, fiber must wait
      @waiting_fibers << Fiber.current
      Fiber.yield  # Suspend until connection available
      @connections.pop
    end
  end
  
  def release_connection(connection)
    if waiting_fiber = @waiting_fibers.shift
      # Resume waiting fiber with connection
      @connections.push(connection)
      waiting_fiber.resume
    else
      @connections.push(connection)
    end
  end
  
  def create_connection
    # Mock database connection
    { id: rand(1000), connected: true }
  end
end

# Web application pattern
class WebApp
  def initialize
    @db_pool = FiberConnectionPool.new(3)
  end
  
  def handle_request(user_id)
    Fiber.new do
      @db_pool.with_connection do |conn|
        # Simulate database query
        Fiber.yield  # Yield during I/O
        user_data = fetch_user(conn, user_id)
        
        # Process user data
        response = generate_response(user_data)
        Fiber.yield response
      end
    end
  end
  
  def fetch_user(connection, user_id)
    { id: user_id, name: "User #{user_id}" }
  end
  
  def generate_response(user_data)
    "Hello, #{user_data[:name]}!"
  end
end

app = WebApp.new
request_fibers = []

# Simulate concurrent requests
5.times do |i|
  fiber = app.handle_request(i)
  request_fibers << fiber
  fiber.resume  # Start the request
end

# Process requests to completion
while request_fibers.any?(&:alive?)
  request_fibers.each do |fiber|
    if fiber.alive?
      result = fiber.resume
      puts "Response: #{result}" if result && result != :waiting
    end
  end
end

Reference

Core Methods

Method Parameters Returns Description
Fiber.new(&block) block (Proc) Fiber Creates new fiber with execution block
Fiber.yield(value=nil) value (Object) Object Suspends current fiber, returns value to caller
Fiber.current None Fiber Returns currently executing fiber
#resume(*args) args (splat) Object Resumes fiber execution with arguments
#alive? None Boolean Returns true if fiber can be resumed
#transfer(*args) args (splat) Object Transfers control to fiber without resume stack

Fiber States

State Description Methods Available
Suspended Fiber created but not started, or yielded #resume, #transfer, #alive?
Running Fiber currently executing Fiber.yield, Fiber.current
Terminated Fiber completed or raised exception #alive? only

Storage Methods

Method Parameters Returns Description
Fiber[key] key (Object) Object Retrieves fiber-local variable
Fiber[key] = value key, value (Object) Object Sets fiber-local variable
Fiber.current.keys None Array Returns array of fiber-local keys

Exception Hierarchy

StandardError
├── FiberError
│   ├── Cannot resume dead fiber
│   ├── Cannot yield from root fiber
│   └── Cross-thread fiber access
└── SystemStackError
    └── Stack level too deep (fiber recursion)

Creation Patterns

Pattern Code Example Use Case
Generator Fiber.new { loop { Fiber.yield value } } Infinite sequences
Iterator `Fiber.new { collection.each { item
Processor `Fiber.new { input
State Machine Fiber.new { state_transitions } Complex control flow
Coroutine Fiber.new { bidirectional_communication } Cooperative tasks

Performance Characteristics

Aspect Fiber Thread Description
Memory ~4KB stack ~1-2MB stack Fiber stacks much smaller
Creation ~1μs ~100μs Fiber creation 100x faster
Context Switch ~10ns ~1μs Fiber switches 100x faster
Scaling 10,000+ 100-1,000 More fibers per process

Common Gotchas

Issue Problem Solution
Root Fiber Yield Fiber.yield in main thread Move logic to created fiber
Cross-thread Resume Resuming fiber from different thread Use same thread for creation/resume
Double Resume Resuming terminated fiber Check #alive? before resume
Infinite Loops Fiber never yields in loop Add strategic Fiber.yield calls
Stack Overflow Deep recursive fiber calls Implement tail call optimization
Memory Leaks Holding references to completed fibers Clear fiber references after completion

Integration Patterns

Framework Pattern Implementation
Rack Middleware with fiber scheduling Wrap app call in fiber context
EventMachine Fiber-aware callbacks Use EM::Synchrony or custom scheduler
Rails Controller action fibers Background job processing with fibers
Sinatra Route handler fibers Async response with fiber coordination