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 |