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| r3A 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 doneInternally Ractor.new (see ractor.rb):
- Allocates a new
rb_ractor_tand sets up its GVL. - Compiles the block under "Ractor mode" (some operations are restricted).
- Spawns a thread within the new Ractor to run the block.
- 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, frozenString, frozen deeply-immutableArray/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.takeblocks the caller,Ractor.yield(v)from insiderunblocks it. Tightly coupled: think generator + caller. - Send/receive:
r.send(v)enqueues into r's mailbox,Ractor.receiveinsiderdequeues. - Closed/timeouts:
r.close_incoming,r.close_outgoing. Receiving from a closed port raisesRactor::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: valuePer-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 raiseRactor::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.rbfor the Ruby-level surface,ractor.cfor C primitives,ractor_sync.cfor blocking semantics. - Shareability rule change:
ractor.c::rb_ractor_shareable_pand 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.