Open-Source Wikis

/

Ruby

/

Systems

/

Fibers and coroutines

ruby/ruby

Fibers and coroutines

Fibers are cooperative coroutines: lightweight, single-threaded, manually-scheduled stacks that pass control to each other via Fiber#resume and Fiber.yield. CRuby implements them by saving and restoring CPU registers and stack pointers in hand-written assembly under coroutine/.

Purpose

  • Provide Fiber.new, Fiber#resume, Fiber.yield, and Fiber#raise.
  • Power generators, lazy enumerators, and cooperative IO.
  • Underpin the Fiber Scheduler interface for asynchronous IO without OS threads.
  • Run with much lower overhead than OS threads (each Fiber's stack starts at ~32 KB, growable on demand).

Files

Path Purpose
cont.c High-level Fiber + Continuation implementation. ~111 KB.
coroutine/ Per-architecture context-switch assembly.
coroutine/<arch>/Context.S The actual register save/restore.
coroutine/<arch>/Context.h C-side struct definition.
ext/fiber/ The pure-Ruby Fiber-related extensions (e.g., Fiber::Pool).
scheduler.c The Fiber Scheduler interface.

Coroutine implementations

coroutine/ contains ~15 architecture/ABI variants:

Directory Target
coroutine/x86/ i386 (System V)
coroutine/amd64/ x86_64 System V (Linux, BSD, macOS)
coroutine/arm32/ 32-bit ARM (EABI)
coroutine/arm64/ AArch64 (Linux, macOS)
coroutine/ppc64le/ 64-bit PowerPC (little-endian)
coroutine/riscv64/ RISC-V 64-bit
coroutine/s390x/ IBM z/Architecture
coroutine/loongarch64/ LoongArch
coroutine/mips64/ MIPS64
coroutine/sparc64/ SPARC v9
coroutine/win32/ i386 Windows
coroutine/win64/ x86_64 Windows
coroutine/copy/ Generic stack-copying fallback (slow but portable)
coroutine/ucontext/ POSIX ucontext_t fallback
coroutine/emscripten/ WebAssembly via Emscripten

Each Context.S defines two routines:

  • coroutine_transfer(from, to) — save callee-saved registers and SP into from, restore from to.
  • coroutine_initialize(context, start, stack_top) — initialise a fresh stack with a callable.

The C-side header:

struct coroutine_context {
    void *stack_pointer;
    void *argument;        /* pointer to the calling context for transfer back */
    /* + arch-specific state (e.g. TLS pointer) */
};

void coroutine_initialize(struct coroutine_context *ctx, coroutine_start start, void *stack, size_t size);
struct coroutine_context *coroutine_transfer(struct coroutine_context *current, struct coroutine_context *target);

The whole point is portability: pure assembly avoids the per-OS quirks of setjmp/longjmp or ucontext.

High-level Fiber API

cont.c builds Fibers on top of coroutines:

typedef struct rb_fiber_struct {
    rb_context_t cont;                   /* shared with Continuation */
    struct coroutine_context coroutine;
    enum fiber_status status;
    rb_execution_context_t saved_ec;     /* saved when not running */
    bool first_proc;                     /* root fiber? */
    /* ... */
} rb_fiber_t;

A Fiber owns:

  • Its own value stack (saved_ec.vm_stack).
  • Its own native stack (allocated via mmap or malloc, depending on platform).
  • A coroutine context.

Fiber.new { |arg| ... } allocates the structures, initialises the coroutine to run a tiny C trampoline that calls the block, and parks it.

f.resume(arg):

  1. Saves the calling Fiber's EC.
  2. Calls coroutine_transfer(current_ctx, fiber_ctx).
  3. Returns whatever was passed to the matching Fiber.yield from inside f.

Fiber.yield(v) is the inverse: save current, transfer back to the caller.

sequenceDiagram
    participant Caller
    participant FiberCtx as Fiber's coroutine context
    participant Block as Fiber block
    Caller->>FiberCtx: Fiber.new { ... }
    Caller->>Caller: stay running
    Caller->>FiberCtx: f.resume(arg)
    FiberCtx->>Block: invoke block(arg)
    Block->>FiberCtx: Fiber.yield(v)
    FiberCtx->>Caller: f.resume returns v
    Caller->>FiberCtx: f.resume(arg2)
    FiberCtx->>Block: yield returns arg2
    Block-->>FiberCtx: block returns final
    FiberCtx-->>Caller: f.resume returns final

Fiber Scheduler

scheduler.c implements the Fiber Scheduler interface. Setting one (Fiber.set_scheduler(s)) makes Ruby's blocking IO calls cooperate with s:

  • IO#read/#write ask the scheduler io_wait(io, events, timeout) instead of blocking.
  • Kernel#sleep calls kernel_sleep(duration).
  • Process.wait calls process_wait(pid, flags).
  • Mutex#lock, ConditionVariable#wait, Queue#pop route through block / unblock.

The most common scheduler is the async gem, which provides an event-loop-driven scheduler that lets thousands of IO-bound Fibers coexist on a single thread.

The C side calls rb_scheduler_* helpers (in scheduler.c) that:

  • Check Fiber.scheduler for the current Ractor.
  • If set, call into the Ruby-level scheduler.
  • If not, fall back to a blocking syscall (with GVL release).

Continuations (legacy)

cont.c also implements Continuation — full call/cc, captured via Continuation.new. Continuations save the entire C stack at capture time and restore it on #call. They're slow, leak-prone, and discouraged since 1.9; Fibers are the modern replacement. The implementation still ships because some legacy code uses it.

require 'continuation' is required to make Continuation.new work — the class isn't autoloaded.

Stack size

Default Fiber stacks are ~32 KB (configurable via RUBY_FIBER_VM_STACK_SIZE and RUBY_FIBER_MACHINE_STACK_SIZE env vars). The VM stack and the native (machine) stack are sized separately because the VM stack stores VALUEs and grows differently from the C stack.

Stacks are allocated lazily and reused via a per-Ractor pool. Killed Fibers' stacks are recycled instead of munmapped.

Performance characteristics

  • Fiber creation: hundreds of nanoseconds.
  • Fiber switch: tens of nanoseconds (a single coroutine_transfer plus EC save/restore).
  • Memory: ~32 KB native stack + ~4 KB VM stack per fiber.

This makes 100,000+ concurrent Fibers feasible — far more than threads, where the OS itself caps you at much smaller numbers.

Entry points for modification

  • New architecture support: add coroutine/<arch>/Context.{h,S}, register it in coroutine/Makefile.in and the configure script.
  • Bug in resume/yield: cont.c::fiber_switch is the central handoff. The arch assembly is correct in isolation; bugs are usually in the EC save/restore.
  • Scheduler interface change: extend scheduler.c and document on the public Ruby API.
  • Stack tuning: env vars are read in cont.c::fiber_machine_stack_size/fiber_vm_stack_size.

See threading.md for OS threads and systems/io.md for how IO calls hand off to the scheduler.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Fibers and coroutines – Ruby wiki | Factory