Open-Source Wikis

/

Ruby

/

Systems

/

Ractors

ruby/ruby

Ractors

Ractors are Ruby's actor-style parallel execution primitive. Introduced in Ruby 3.0 (December 2020), each Ractor has its own GVL, so threads inside different Ractors can run truly in parallel — no shared mutable state, communication via copy or move.

Purpose

Provide a parallel-by-default unit of execution that doesn't require fine-grained locking inside the VM. Trade off shared-memory convenience for parallelism: only shareable objects (frozen and deeply frozen, or special types) can cross Ractor boundaries.

Files

File Purpose
ractor.c Core implementation. ~70 KB.
ractor.rb Ruby-level API on top of C primitives.
ractor_core.h rb_ractor_t definition and helpers.
ractor_sync.c Synchronisation: ports, channels, blocking. ~35 KB.

Architecture

graph TD
    proc[Ruby process] -->|owns| vm[rb_vm_t]
    vm -->|hosts| r1[Ractor 1]
    vm -->|hosts| r2[Ractor 2]
    vm -->|hosts| r3[Ractor 3]
    r1 -->|owns| t1a[Thread 1.a]
    r1 -->|owns| t1b[Thread 1.b]
    r1 -->|owns| gvl1[GVL 1]
    r2 -->|owns| t2[Thread 2]
    r2 -->|owns| gvl2[GVL 2]
    r3 -->|owns| t3[Thread 3]
    r3 -->|owns| gvl3[GVL 3]
    r1 <-->|send / take| r2
    r2 <-->|yield / receive| r3

A Ractor is roughly a self-contained mini-VM:

struct rb_ractor_struct {
    rb_global_vm_lock_t *gvl;            /* this ractor's GVL */
    struct list_head threads;            /* threads owned by this ractor */
    rb_thread_t *threads_running;
    /* per-ractor constant cache, IV cache, frozen string cache */
    /* incoming message queue, outgoing yields */
    /* ID counter, name */
};

All Ractors share the rb_vm_t (the process), but each has its own:

  • GVL.
  • Constant cache version.
  • Inline-cache state.
  • Thread list.
  • Default error reporter.
  • Random seed.

Top-level constants are global (one per process), but Ractors carry per-Ractor constant caches that invalidate on definition.

Creating a Ractor

r = Ractor.new do
  # this block runs in a new Ractor
  100.times.sum
end

r.value  # blocks until done

Internally Ractor.new (see ractor.rb):

  1. Allocates a new rb_ractor_t and sets up its GVL.
  2. Compiles the block under "Ractor mode" (some operations are restricted).
  3. Spawns a thread within the new Ractor to run the block.
  4. Sets up message ports.

Sharing rules

By default, Ractors share no mutable state. The shareability rules are checked dynamically:

  • Frozen objects with no mutable fields are shareable (Integer, Symbol, Float, frozen String, frozen deeply-immutable Array/Hash).
  • Ractor.make_shareable(obj) deep-freezes an object so it crosses boundaries.
  • Class and Module objects are shareable (they're already process-global).
  • All other objects must be copied (Marshal-based) or moved (the source-side reference becomes unusable).

The shareability check is in ractor.c::rb_ractor_shareable_p and is invoked anywhere an object would cross a Ractor boundary (channels, take/yield, instance vars on shared objects).

Communication

Ractors talk through ports:

  • Take/yield: r.take blocks the caller, Ractor.yield(v) from inside r unblocks it. Tightly coupled: think generator + caller.
  • Send/receive: r.send(v) enqueues into r's mailbox, Ractor.receive inside r dequeues.
  • Closed/timeouts: r.close_incoming, r.close_outgoing. Receiving from a closed port raises Ractor::ClosedError.

ractor_sync.c implements the blocking and signaling. The internals are a per-Ractor wait queue with native condition variables.

sequenceDiagram
    participant A as Ractor A
    participant Bport as Ractor B mailbox
    participant B as Ractor B
    A->>Bport: r.send(value)  [copy or move]
    A->>A: returns immediately
    B->>Bport: Ractor.receive
    Bport-->>B: value

Per-Ractor caches

Several CRuby caches that used to be process-global are now per-Ractor to avoid GVL serialisation across Ractors:

  • Inline caches (callcache, IC for IVs, IC for constants).
  • Constant cache version — bumped when a constant changes; checks invalidate per-Ractor.
  • Frozen string literal pool — stays per-Ractor so two Ractors don't fight over the table.
  • Random source — each Ractor seeds its own.

The trade-off: if a constant is defined and then read in many Ractors, each Ractor pays the first-miss cost.

Ractor mode and restrictions

When a method is being JIT-compiled or called from a non-main Ractor:

  • It cannot read/write global variables that aren't Ractor.make_shareable-d.
  • It cannot define new methods on shared classes (the change wouldn't be visible across Ractors safely).
  • Object.const_set, Module#define_method, etc. on shared classes raise Ractor::IsolationError.

These checks are enforced in vm_method.c and variable.c via the rb_ractor_main_p() predicate.

Cooperation with the GC

The GC walks every Ractor's stack and registered roots. gc/default/gc.c::gc_mark_roots iterates the VM's Ractor list and marks each Ractor's threads. Per-Ractor caches that are GC-eligible (e.g., Method entries cached by class) participate in marking.

A rb_vm_barrier (in vm_sync.c) stops every Ractor's running thread before performing global mutations (e.g., new method definitions on shared classes, GC compaction). This is the coordination primitive that keeps the VM consistent.

Performance and limitations

  • Ractor startup is heavier than Thread startup (allocation of a new GVL, caches, message queue).
  • Crossing a Ractor boundary requires shareability checking, which can be expensive for non-frozen graphs.
  • The VM still has process-global state (encodings, the symbol table, the class hierarchy) that must be coordinated.
  • As of this snapshot, Ractor is still labeled experimental in some respects — APIs are evolving.

Entry points for modification

  • New Ractor primitive: extend ractor.rb for the Ruby-level surface, ractor.c for C primitives, ractor_sync.c for blocking semantics.
  • Shareability rule change: ractor.c::rb_ractor_shareable_p and the type-specific checks (rb_str_shareable_p, rb_ary_shareable_p).
  • Per-Ractor cache: store on rb_ractor_t, invalidate via the appropriate API. Examples: rb_ractor_constant_cache, rb_ractor_global_cache.
  • Cross-Ractor coordination: use rb_vm_barrier (vm_sync.c) when you need to atomically mutate global state.

See threading.md for the GVL details and reference/data-models.md for the VM data layouts.

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

Ractors – Ruby wiki | Factory