ruby/ruby
Threading and the GVL
Ruby has native (kernel) threads. They share memory but are coordinated by a per-Ractor Global VM Lock (GVL) that ensures only one thread per Ractor runs Ruby bytecode at a time. The GVL is released around blocking syscalls so I/O and OS calls run in parallel even when the interpreter does not.
Purpose
- Provide
Thread.new,Thread#join,Thread#kill,Mutex,ConditionVariable,Queue,SizedQueue,Monitor. - Maintain memory-safety in the absence of fine-grained locking inside the VM by serialising bytecode execution through the GVL.
- Release the GVL when a thread is going to block in the OS so other Ruby threads can run.
- Cooperate with Fibers (see fiber.md) and Ractors (see ractor.md).
Files
| File | Purpose |
|---|---|
thread.c |
Platform-independent thread API. ~177 KB. |
thread_pthread.c |
POSIX thread backend. ~103 KB. |
thread_pthread.h |
POSIX thread struct layouts. |
thread_pthread_mn.c |
Optional M:N scheduler for pthread builds. |
thread_win32.c |
Windows thread backend. ~25 KB. |
thread_win32.h |
Windows-specific structs. |
thread_none.c |
Stub backend for builds with --disable-threads. |
thread_sync.c |
Mutex, ConditionVariable, Queue, SizedQueue. |
thread_sync.rb |
Ruby-level wrappers (Mutex#synchronize, etc.). |
signal.c |
POSIX signal delivery and translation to interrupts. |
vm_sync.c |
VM-wide barriers for safe global mutation. |
scheduler.c |
The Fiber Scheduler interface (used by IO blocking calls). |
Architecture
graph TD
rb_thread_t -->|owns| native[Native pthread / Windows thread]
rb_thread_t -->|reads/writes| ec[rb_execution_context_t]
rb_thread_t -->|holds| status[Status: running / sleeping / waiting]
rb_thread_t -.->|GVL handoff| gvl[Per-Ractor GVL]
gvl -->|lets one| running[Currently running thread]
running -->|releases on syscall| nogvl[rb_thread_call_without_gvl region]
nogvl -->|reacquires after| gvl
Ractor -->|owns one| gvlEach Ruby thread is an rb_thread_t (vm_core.h) that owns:
- A native OS thread (
th->thread_idon POSIX,th->thread_idon Windows). - An
rb_execution_context_t(the EC) holding its VM stack and current frame pointer. - A pointer to its Ractor.
- An interrupt mask, scheduler hook, and last error.
Threads belong to exactly one Ractor at any time, and Ractors do not share threads. The GVL is per-Ractor, so multiple Ractors run in parallel, but threads inside a Ractor serialise.
The GVL
The GVL (Global VM Lock) is a mutex + condition variable per Ractor. Roughly:
- Only the thread holding the GVL may execute Ruby bytecode.
- A thread that's about to block in a syscall releases the GVL, performs the syscall, then reacquires before touching Ruby state.
- A periodic timer (
thread_pthread.c::timer_thread) fires every ~10 ms; the running thread checks an interrupt flag and yields the GVL if another thread is waiting.
Implementation details:
- POSIX:
gvl_acquire/gvl_releaseinthread_pthread.c. Uses a futex-backed mutex on Linux. - Windows:
gvl_acquire/gvl_releaseinthread_win32.c. Uses Windows Events.
The GVL handoff cadence is tunable via RUBY_THREAD_TIMESLICE (default 100 ms in some builds, less elsewhere). Tuning it is rarely necessary.
Releasing the GVL: rb_thread_call_without_gvl
Any C code that's about to block in the OS must release the GVL:
struct args { int fd; void *buf; size_t len; ssize_t ret; int errnum; };
static void *
do_blocking_read(void *p)
{
struct args *a = p;
a->ret = read(a->fd, a->buf, a->len);
if (a->ret < 0) a->errnum = errno;
return NULL;
}
static VALUE
my_read(VALUE io, VALUE n)
{
struct args a = { ..., 0, 0 };
rb_thread_call_without_gvl(do_blocking_read, &a, RUBY_UBF_IO, NULL);
if (a.ret < 0) rb_syserr_fail(a.errnum, "read");
return n_to_str(&a);
}The unblock function (RUBY_UBF_IO here) is what Thread#kill and signal delivery call to interrupt the syscall mid-flight. Predefined ones include RUBY_UBF_IO, RUBY_UBF_PROCESS, RUBY_UBF_DEFAULT. Not all syscalls are interruptible; passing NULL says "this region is not interruptible" and is a footgun — prefer one of the canned UBFs.
Blocking primitives
thread_sync.c implements:
Mutex(a fair queue of waiters).ConditionVariable(#wait,#signal,#broadcast).QueueandSizedQueue— thread-safe blocking queues.
The Ruby-level Mutex#synchronize, Queue#pop, etc. are wrappers in thread_sync.rb.
Monitor (re-entrant mutex) is in lib/monitor.rb on top of Thread::Mutex.
Signals
POSIX signals are caught by a one-shot handler in signal.c::sighandler that:
- Records the signal number in a per-VM ring buffer.
- Wakes the timer thread.
The next "interrupt check" (which happens at every method call and on iseq transitions) drains the buffer and invokes Ruby-level signal handlers via rb_signal_exec.
This indirection lets Ruby-level handlers run safely (allocating memory, calling other Ruby code) — the C handler itself does almost nothing.
M:N scheduler
thread_pthread_mn.c implements an optional M:N scheduler that multiplexes Ruby threads onto a smaller pool of native threads. Enable it with RUBY_MN_THREADS=1:
- Ruby threads become lightweight; they live on a per-Ractor user-space scheduler.
- A small pool of "service" pthreads picks them up when one is runnable.
- Blocking syscalls move a thread out of the user-space scheduler temporarily.
This is most useful when an application has many threads (hundreds or thousands) that mostly block on IO. It's still considered experimental and is off by default.
Fiber Scheduler integration
scheduler.c is the Ruby-level Fiber Scheduler interface. When an event-loop gem (e.g., async) installs a scheduler:
Fiber.set_scheduler(MyScheduler.new)Then blocking IO calls (read, write, sleep, getaddrinfo, mutex wait) defer to scheduler hooks, which can suspend the current Fiber and resume it when the IO event arrives. This makes high-concurrency IO programs possible without one OS thread per connection.
The scheduler interface methods are:
io_wait(io, events, timeout)kernel_sleep(duration)process_wait(pid, flags)address_resolve(hostname)block(blocker, timeout)/unblock(blocker, fiber)
scheduler.c::rb_scheduler_* C helpers route C-level calls through the scheduler when one is set.
Thread interruption
Thread#raise, Thread#kill, Thread#wakeup all post an interrupt to the target thread. The interrupt is delivered:
- Immediately, if the thread is blocked in
rb_thread_call_without_gvlwith a UBF — the UBF wakes it. - At the next interrupt check, if the thread is running Ruby code.
Thread.handle_interrupt lets a thread defer interrupts by class (e.g., "ignore Timeout::Error while inside this block").
Entry points for modification
- Threading bug:
thread.cfor the public API;thread_pthread.c/thread_win32.cfor the platform glue.thread_sync.cfor blocking primitives. - GVL behaviour:
gvl_acquire/gvl_releasein the platform file. The yield logic is inthread_pthread.c::do_gvl_timer_start_if_needed. - Signals:
signal.c::sighandlerandrb_signal_exec. - Adding a scheduler hook: extend the scheduler interface in
scheduler.c. The Ruby-level methods are documented at https://docs.ruby-lang.org/en/master/Fiber/Scheduler.html.
See ractor.md for parallel execution and fiber.md for cooperative coroutines.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.