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 |