Under the M:N thread scheduler, a Ruby thread in a non-main Ractor may be
resumed on a different native thread after it blocks. The C compiler
assumes that the native thread never changes within a function, so it may
compute the address of ruby_current_ec once (`__tls_get_addr` in
libruby.so) and reuse it after the switch. The address then refers to the
TLS of the previous native thread, which leads to a SEGV or to using
another thread's ec.
8.times.map do
Ractor.new do
50.times do
",,".split(/(,)/) { sleep 0.001 }
end
:ok
end
end.map(&:value)
#=> [BUG] Segmentation fault at 0x0000000000000030 (with Clang 21)
arm64 and ppc64le already use the out-of-line rb_current_ec()
(RB_THREAD_CURRENT_EC_NOINLINE), which prevents the compiler from caching
the address. Use it on all platforms, and remove the macro.
[Bug #22453]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruby_nt_serial and RUBY_NT_SERIAL were declared only when
RB_THREAD_CURRENT_EC_NOINLINE is not defined, so RUBY_DEBUG_LOG() did not
show the native thread serial on arm64 and ppc64le. ruby_nt_serial is not
related to the way ruby_current_ec is accessed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows carried a second, much weaker scheduler: a single Win32 mutex as
the GVL, vm->clock for preemption, its own VM barrier over
vm->ractor.sync.barrier_*, its own Ractor wait/wakeup on a per-Ractor
condvar, no ready queue and no time slice. Keeping two made the rest of
the interpreter branch on which one was compiled in, and left Ractors on
Windows without real scheduling.
Move the scheduler out of thread_pthread.c into thread_sched.c and let
both platforms use it. thread.c now includes thread_sched.c, which
declares the scheduler <-> platform contract, includes THREAD_IMPL_SRC,
and then builds the scheduler on the primitives that file provides.
thread_pthread.c keeps only those primitives: synchronization, thread
local storage, native thread creation and stacks, the communication pipe,
the timer thread backend, the fork lock and the event hooks.
The data structures the scheduler works on move to a new thread_sched.h,
included from both platform headers, so struct rb_thread_sched,
struct rb_native_thread and struct rb_thread_sched_item are now shared.
thread_pthread_mn.c becomes thread_sched_mn.c: it is the M:N scheduler,
not a pthread detail. Most of it -- the coroutine threads, the native
thread stack pool, the timer wheel and the fd -> waiters map -- is
platform independent; only the readiness backend is not, and that section
is now marked as the seam where an epoll/kqueue split (or an IOCP backend,
which is what Windows would need for M:N) would go.
What Windows had to add is small: native_thread_interrupt(),
native_thread_default_max_cpu(), timer_thread_polling(),
timer_thread_wakeup_force(), RB_NATIVE_COND_MONOTONIC_P() and
RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF() -- the last because a
CRITICAL_SECTION is recursive, so trylock cannot back a "somebody holds
this" assertion. It sets USE_MN_THREADS to 0 and supplies the stubs
thread_sched_mn.c provides elsewhere, so every thread there is dedicated,
as it already was.
With one scheduler the platform branches go away: all 16 uses of
RUBY_THREAD_PTHREAD_H / RUBY_THREAD_WIN32_H are removed from vm_core.h,
ractor.c, ractor_core.h, ractor_sync.c, vm_sync.c, vm.c and thread.c.
ractor_check_blocking() went with them -- it only ever fed the win32
scheduler's vm->ractor.blocking_cnt -- and so did USE_VM_CLOCK, now that
the common time slice drives preemption on Windows too.
Verified on both platforms. Linux: 11 rounds of make test-all (36236
tests) and btest (2066 tests) with no failures, plus 474 runs of a
scheduler and Ractor stress soak. Windows: no test fails that does not
also fail on the same tree without this change, btest passes, and two
40-minute soaks of 438 and 460 runs are clean. Ractors now run in
parallel there -- 3.6x on four Ractors, where before they could not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every io wait took timer_th.waiting_lock to register and again to wake:
one global lock serialized the fd map, the epoll_ctl calls made under
it, the timer wheel and the wake-pending flags, across all fds.
Split the ownership:
- an fd's waiter list, arming state and the flags of its io waits move
under one of 16 fd shard locks; unrelated fds register and wake in
parallel, and the epoll_ctl calls leave the global lock entirely
- waiting_lock keeps the wheel and fd-less timed waits
- wake_pending becomes a count under its own small lock, so an expiry
hold and an fd event's wake can pin one thread at once
- the untimed waiter list is dropped (its only reader was a VM_ASSERT
pass), so untimed io waits touch no global lock at all
- the fd map's chunk table becomes a fixed array with CAS-installed
chunks, since chunks span shards
An io+timeout wait is registered in both structures; whoever clears its
flags under the fd shard owns the wakeup. The expiry pass pops wheel
nodes under waiting_lock, pins the thread with the pending count, and
claims under the shard by (serial, fd) -- lock order is always
shard -> waiting_lock -> wake_pending_lock.
R ractor pairs (1 thread each) ping-ponging over pipes with blocking
reads, total round-trips/sec, 16-HT machine (Ryzen 9 5900HX),
RUBY_MN_THREADS=1, mean of 2 alternating same-tree runs:
before after
8 pairs 63k 134k
16 pairs 64k 143k
Messaging without fds (port ping-pong pairs) is unchanged.
A thread-heavy shape (256 threads in few ractors doing memcached round
trips) is unchanged: its wakes stay inside each ractor's readyq and the
old lock was not saturated there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thread_sched_setup_running_threads runs on every context switch (park,
resume, and both ends of every blocking region) and took the global
ractor.sched.lock to maintain the running_threads and timeslice lists
and running_cnt. With many ractors switching concurrently this lock is
the scaling ceiling.
Replace all three with ntlist, a per-location registration of who runs
where, and no shared counter:
- a thread on a shared nt registers in that nt's running_th, under the
nt's own lock -- uncontended, no cross-core traffic
- a dedicated nt goes on ntlist.running_dnts while its thread runs (the
switch rate of dedicated threads is what it always was; only the lock
shrank), so the fast path writes no shared cache line at all
The consumers walk the registrations: the barrier's interrupt walk also
counts them into a snapshot (barrier_running_cnt) and stamps each nt
with the barrier serial; a stamped nt deregistering during that barrier
decrements the snapshot under sched.lock, so the completion check stays
O(1) however many runners there are.
The timeslice list becomes timeslice.scheds, the scheds whose readyq
holds waiters. The enq that fills an empty readyq (and the add path
taking a turn with waiters already queued) links the sched; the timer
thread interrupts each listed sched's running thread and prunes entries
whose readyq drained. A steadily contended sched stays linked, so the
switch path never touches the list, and idle scheds are not walked at
all. The timer takes each sched's lock by trylock, because the
switchers nest sched.lock -> timeslice.lock; pruning is lazy, so
ractor_free delists the dying ractor's sched.
Barrier pairing needs no fences: the barrier sets barrier_is_waiting
and then walks the registrations under their locks; a switch moves its
registration under the same lock and then reads the flag. A walk that
missed a registration ran before that registration's critical section,
so its flag store is visible to that switcher. Nothing may start
running while the flag stands -- an interrupt flag alone would let a
resuming thread run, and allocate, inside the stop-the-world section --
so the add path takes its registration back and waits for
rb_ractor_sched_barrier_end's broadcast. While a scan holds a
running_th lock the registered thread cannot finish parking, so it
cannot die under the scan. An snt turned dedicated under
rb_thread_lock_native_thread has no creation-time running_thread, so
the running_dnts registration writes nt->running_thread itself.
R ractor pairs ping-ponging via ports (msgs/sec) and R ractors doing
pipe write/read round-trips (rt/sec), 16-HT machine (Ryzen 9 5900HX),
mean of 2 alternating same-tree runs:
before after
port pairs 8 503k 815k +62%
port pairs 32 453k 799k +76%
pipe self 4R 837k 940k +12%
pipe self 16R 820k 898k +10%
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dedicated native thread used to sleep in two stages: native_cond_sleep()
parked it on nt->cond.intr inside a blocking region, and on wakeup it went
back to the scheduler to wait for its running turn on nt->cond.readyq. The
thread scheduler's turn wait takes an absolute deadline since d32793fe8e, so
the sleep can happen right there: native_sleep() now parks every dedicated
thread in thread_sched_to_waiting_until_wakeup(), with a deadline when it
has one, and an M:N thread keeps using the timer wheel. The second condvar,
its ubf and native_cond_sleep() go away, and with them the union/struct
dance in struct rb_native_thread for platforms whose condvars remember
their mutex: the one condvar left always pairs with sched->lock_.
Two things keep the handoff as fast as the old path:
The turn wait trusts ETIMEDOUT to say the deadline passed, instead of
reading the clock on every wakeup; on clocksources where clock_gettime is a
real syscall that read was the single biggest cost of a timed wakeup.
ubf_waiting() wakes the target even when the running turn is taken, knowing
it re-parks at once. The old two-stage sleep did the same thing by its
shape, and it is worth doing on purpose: the woken thread's futex wakeup
runs on another core in parallel with the running thread, so by the time
the turn is handed over it is off the handoff path. Measured on a
queue-with-timeout ping-pong, the handoff costs what the two-stage sleep
cost; without the early wake it was three times slower.
The ubf must also leave alone a thread whose deadline already put it back
in the ready queue: waking it a second time would double-enqueue it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The timer thread delivers expiry and fd wakeups in batches: it collects
{thread, serial} pairs under timer_th.waiting_lock, releases the lock (the
scheduler lock a wakeup takes must not nest inside it), and then wakes each
thread. Unlinking an entry is what releases its thread, so from that moment
the thread can be woken by somebody else, exit and be freed while the batch
still holds a bare pointer to it. Waking it then dereferences a freed
thread, and crashes when the Ractor holding it was torn down:
[BUG] Segmentation fault at 0x0000000000000138
timer_thread_check_timeout -> timer_thread_wakeup_thread
-> rb_native_mutex_lock(&TH_SCHED(th)->lock_) # th->ractor is NULL
The serial captured in the batch does not help, and can even match again: a
thread struct reused from the freed one starts counting event serials from
zero, so its first timed wait matches serial 1 held in a stale batch entry,
and the timer wakes a thread whose wheel entry is still armed:
Assertion Failed: thread_sched_wait_running_turn:
th->sched.waiting_reason.flags == thread_sched_waiting_none
Both reproduce on a loop of Ractors that die while one of their threads sits
in a timed receive, in ~15 rounds of 400. They only became reachable when
e1bce29aac cut a dying Ractor's teardown from one second to well under a
millisecond: the batch window used to be dwarfed by the teardown time.
Holding waiting_lock across the wakes would close the window but deadlock:
arming a timer takes the scheduler lock and then waiting_lock, so a wake
taking the scheduler lock under waiting_lock inverts the order. So mark the
threads instead. When the timer thread publishes a batch it sets
in_wake_batch on each thread, under waiting_lock; when it has woken them all
it clears the marks and broadcasts. A dying thread checks its own mark and
waits on the cond until it clears, fencing twice:
Once when it leaves the scheduler for good (thread_sched_to_dead, and the
coroutine epilogue in thread_start_func_2). This is the fence that matters
for a dying Ractor: a stale wakeup reaches the Ractor through TH_SCHED(),
and the rb_ractor_t can be collected as soon as the Ractor is unlinked, so
the wait must happen while it is still alive. Nothing re-arms the thread
after this point, so no later batch can name it.
Once more when the rb_thread_t itself is freed (rb_threadptr_sched_free), as
a backstop for frees that do not come through a thread's own exit.
The wait is bounded by one batch of at most 16 wakeups, and the fence takes
only waiting_lock, which the timer thread never holds while it wakes, so the
two cannot deadlock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timer_thread_register_waiting kept the timed waiters on a single list
sorted by deadline, inserting with a linear scan. A server whose
connections share one timeout value -- every keep-alive loop -- makes
each insert walk the whole list: with 8000 waiting connections the
scan burns half the CPU (43.71% + 8.30% in thread_sched_wait_events)
and M:N falls behind 1:1.
Bucket timed waiters into a hierarchical timing wheel instead
(Varghese & Lauck, "Hashed and Hierarchical Timing Wheels: Data
Structures for the Efficient Implementation of a Timer Facility",
SOSP '87): 4 levels of 64 slots, 1ms slots at the finest level and
64x coarser per level, ~4.6h of direct range; farther deadlines park
on the coarsest level's farthest slot and re-sort on arrival. A
not-yet-due waiter found while draining a slot is re-bucketed onto a
finer level, so each waiter moves at most once per level. Deadlines
are floored to a slot, so the wheel never fires late, and the
re-bucketing keeps sleep precision.
Insertion becomes O(1) arithmetic. Removal -- the most frequent
operation, since most timed fd waits are cancelled by their fd
becoming ready -- stays O(1): the cached next-expiry is a lower
bound that only insertion has to maintain (waking early is harmless,
so removal does no bookkeeping at all). The timer thread is now
woken only when an insertion shortens that bound, instead of on
every insertion.
Untimed (fd-only) waiters move to their own plain list. The wheel
and its operations are compiled only with USE_MN_THREADS; the slot
width can be widened with -DTIMER_WHEEL_TICK_MS=n for experiments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
native_thread_check_and_create_shared honours max_cpu, but that is only
half of it. snt_cnt also drops whenever a thread enters a blocking
region and rises again when it leaves, and the rejoining side checks
nothing:
native_thread_dedicated_inc: snt_cnt--; dnt_cnt++;
native_thread_dedicated_dec: snt_cnt++; dnt_cnt--; /* no cap */
So while a thread is away in a read(2) the timer thread reads the dip as
a shortage and hires a replacement, and when the original comes back
nobody is let go. On an HTTP/1.1 Ractor server that transition happens
about once per dispatch, and the pool ratchets past the documented cap:
30 to 52 native threads on 8 cores at RUBY_MAX_CPU=16, where only 16 of
them can run Ruby at once. The surplus does not add throughput. It
drains the global ready queue, so a thread coming back for work finds it
empty and parks, and every dispatch after that is a handoff to a
sleeping thread on another core instead of a continuation on the one
that just freed up -- 2.15 CPU migrations per request against 0.01, 2.2x
the cache misses, IPC 0.51 down to 0.45.
Check the cap where a thread rejoins the shared threads. One that finds
no room left is on its way out: it belongs to neither count and ends
when it next asks for work, which is the first point at which it is not
running anything.
At RUBY_MAX_CPU=16 with 128 connections and up, on requests short enough
for scheduling to be visible:
static response 95,358 -> 204,935 req/s (34 -> 11 threads)
memcached lookup 70,805 -> 115,424 req/s (32 -> 15)
30 such lookups 1,740 -> 6,580 req/s (49 -> 9)
Requests that do 10ms or more of their own work are unaffected in either
direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two threads waiting on one fd -- a reader and a writer on the same socket
is the ordinary shape -- made the second one see a lie.
timer_thread_register_waiting() returned false both when the fd was
already ready and when it could not be registered at all (epoll_ctl
giving EEXIST because another thread holds the registration, or EBADF /
EPERM). thread_sched_wait_events() turned "could not register" into
timedout = false, thread_io_wait_events() turned that into "the event
happened", and thread_io_wait() then fabricated fds[0].revents = events.
So IO#wait_readable returned readable for an fd nothing had written to,
and a blocking read behind another waiter spun at 100% CPU
(read -> EAGAIN -> poll -> epoll_ctl -> EEXIST -> retry) until the fd
really became ready.
Distinguish the three outcomes by type rather than packing them into a
bool, at each of the three layers, so that "not registered" can no longer
arrive as readiness. A caller that gets it falls back to its ordinary
blocking path.
Then stop producing that case at all: keep the waiters of an fd in a
table indexed by the fd (POSIX hands out the lowest free descriptor, so
fds stay dense) and arm the backend with the union of what they ask for,
so a second waiter extends the registration instead of colliding with it.
Entries live in fixed chunks because growing the table must not move a
live list head.
Events now carry the fd and the generation of its table entry instead of
a thread pointer. A stale event -- one epoll_wait had already queued
when the fd was disarmed, possibly closed and reopened since -- resolves
to a table lookup under the lock and is dropped on the generation check,
where before it dereferenced a possibly freed rb_thread_t. Managing the
union also means kqueue deletes the filters it registered rather than
only the one that fired, so a registration can no longer be left behind.
Waiters are unlinked and their results recorded while waiting_lock is
held, but woken after releasing it: the scheduler lock is always taken
before waiting_lock, and capturing event_serial under the lock is what
keeps a thread that has since moved on from being woken, as the timeout
path already does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The nt scheduling loop reclaimed a terminated coroutine thread's context using
the return value of coroutine_transfer() (through thread_sched_switch0),
assuming it was the resuming context (the dead coroutine). That holds for the
amd64 asm backend, but the ucontext backend returns the transfer *target*
instead, so the loop never recognized the terminal transfer: the reclaim always
saw a live thread, and the loop always unlocked the sched lock -- which the
dying thread had already released in coroutine_thread_terminated -- and
double-unlocked it. Benign on glibc, but FreeBSD's pthread_mutex_unlock returns
EPERM (crash), and it corrupts the scheduler (hang) elsewhere.
Have the terminating coroutine record its own context in nt->dead_co (per
native thread) right before its final transfer; the loop reads it after switch0
returns, independent of coroutine_transfer()'s backend-dependent return value.
It is set and read on the same native thread with nothing running in between,
so it cannot be overwritten before it is consumed.
coroutine_transfer0() and thread_sched_switch0() no longer need to return the
resuming context, so revert them to void (the ASan-only local remains).
Reproduced on Linux with --with-coroutine=ucontext (100% hang before, clean
after); amd64 and ucontext both pass bootstraptest thread/ractor/fiber.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redesign the teardown of M:N (coroutine) threads around the natural call
structure nt_start -> co_start -> thread_start_func_2: when the thread body
returns, its epilogue (coroutine_thread_terminated) finishes all rb_thread_t /
rb_ractor_t business while th is still valid, and co_start then makes exactly
one final transfer back to the native thread's own context (nt_start's loop),
where the nt reclaims the dead coroutine's context. The resume itself proves
the final transfer's register save completed, so the reclaim needs no list,
no handshake and no extra locking.
This deletes the zombie-threads machinery (VM-global list, sched.finished
publish, the GC mark/reap pass) and closes its UAF window: publishing
`finished` before the terminal transfer let the GC free the Thread wrapper --
whose coroutine context the transfer still saves into -- mid-teardown.
* struct rb_thread_context bundles a coroutine thread's execution-owned
resources: the coroutine_context (kept first, so th->sched.context points
into the block) plus its machine stack, the stashed final-transfer target
(nt), and a dead flag. co_start's epilogue sets the flag and transfers via
the stashed nt, touching tctx alone -- th may already be collected by then.
The nt loop passes every coroutine it resumes from to
thread_sched_reclaim_from(), which frees dead ones (stack back to the
pool). A live coroutine cannot be resumed elsewhere, die and be freed
while the nt loop still inspects it: a live yield arrives with the sched
lock handed off, and no other nt can schedule that thread again until the
nt loop's own unlock.
* coroutine_thread_terminated (run from thread_start_func_2, where the
upstream non-coroutine teardown also runs, while th is still valid and
listed) does: wake the next thread as upstream's thread_sched_to_dead
does; stash th->nt into tctx and detach th->sched.context; leave the
living set; clear the current ec; and, for a shared-nt non-last thread,
enqueue the Ractor's next runnable thread (not earlier: waking it from
to_dead would let another thread of the same Ractor execute while this
teardown still runs). Everything that reads th happens before the
living-set removal, after which the GC may collect th (and, for a
Ractor's main thread, the Ractor).
* interrupt_lock is now destroyed in thread_free instead of during teardown
(thread_cleanup_func): while th is in its Ractor's living set, anyone --
terminate_all on the Ractor's terminating main thread, Thread#kill /
#raise -- may lock it, and destroying it mid-teardown leaves a window
where a concurrent interrupter locks a destroyed mutex (EINVAL; the
upstream non-coroutine path has the same, smaller, window). At thread_free
time the wrapper is unreachable AND off the living set (listed threads
are marked), so no interrupter can exist. The atfork cleanup reinitializes
the copied lock, which may have been held at the instant of fork.
* The Thread wrapper's dfree only reclaims the context of a thread that
never started; a terminated one cleared th->sched.context in its epilogue.
VM shutdown needs no extra synchronization: past the living-set removal the
epilogue touches only its own context block, the TLS and nt->nt_context,
none of which VM destruct frees (SNT native threads and the machine-stack
pool are intentionally not torn down at exit, as before).
Verified: bootstraptest 2050 PASS; test_thread / test_ractor / test_fiber /
test_gc / test_gc_compact / thread_queue 282 tests 0F0E; Ractor x Thread x
IO teardown churn 150/150 + 60/60 (exercises DNT promotion, terminate_all
racing dying threads, and the double-enqueue fixed in the previous commit);
fork with live Ractors + child GC 20/20; ASAN clean including unreferenced
Thread/Ractor collection (the old zombie window) and mixed stress;
RGENGC_CHECK_MODE=2 btest 2050 + churn; TSan warning profile identical to
master (the new functions appear in no race report); YJIT
move-under-GC.stress; kill/join races; Thread object retention across
Ractor death; exit-while-winding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A runnable thread whose Ractor was enqueued to the global ready queue can
be served without a dequeue: a thread waking up in wait_running_turn hands
its native thread over by a direct transfer, and the hot-thread path
steals `running` back outright. Both leave the Ractor's grq entry behind,
orphaned. The entry itself is harmless (the dequeuing nt sees a served
Ractor and skips), but the accounting breaks: the next legitimate enqueue
double-lists the one grq_node embedded in the sched, corrupting the queue
(observed on master as pthread_mutex_lock EINVAL / crashes; about 1/20
runs of a Ractor x Thread x IO churn under a saturated shared-nt pool).
Restore the invariant "enqueued <=> runnable and unserved" by cancelling
the entry at both direct-service points (ractor_sched_cancel_enq). The
grq_node is kept self-linked whenever it is not enqueued, so membership is
read off the node itself: under the per-Ractor sched lock a self-linked
read is decisive (enqueuers hold that lock too), and the common direct
switch -- whose transition never enqueued -- pays no lock at all; a linked
read can race only with a dequeue, so recheck under the grq lock before
ccan_list_del_init.
ractor_sched_enq now unconditionally rb_bugs on a still-linked node: the
double-list is queue corruption either way, and a VM_CHECK_MODE-only
assert cannot catch this race in practice (CHECK builds perturb the
timing; the previous VM_CHECK-only scan never fired while plain builds
reproduced the corruption).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix race between timer thread dequeuing waiting thread and thread
skipping sleeping due to being dequeued. We now use `th->event_serial` which
is protected by `thread_sched_lock`. When a thread is put on timer thread's waiting
list, the event serial is saved on the item. The timer thread checks
that the saved serial is the same as current thread's serial before
calling `thread_sched_to_ready`.
The following script (taken from a test in `test_thread.rb` used to crash on
scheduler debug assertions. It would likely crash in non-debug mode as well.
```ruby
def assert_nil(val)
if val != nil
raise "Expected #{val} to be nil"
end
end
def assert_equal(expected, actual)
if expected != actual
raise "Expected #{expected} to be #{actual}"
end
end
def test_join2
ok = false
t1 = Thread.new { ok = true; sleep }
Thread.pass until ok
Thread.pass until t1.stop?
t2 = Thread.new do
Thread.pass while ok
t1.join(0.01)
end
t3 = Thread.new do
ok = false
t1.join
end
assert_nil(t2.value)
t1.wakeup
assert_equal(t1, t3.value)
ensure
t1&.kill&.join
t2&.kill&.join
t3&.kill&.join
end
rs = 30.times.map do
Ractor.new do
test_join2
end
end
rs.each(&:join)
```
Add the macro `RB_THREAD_CURRENT_EC_NOINLINE` to manage the condition to use
no-inline version rb_current_ec for a better maintainability.
Note that the `vm_core.h` includes the `THREAD_IMPL_H` by the
`#include THREAD_IMPL_H`. The `THREAD_IMPL_H` can be `thread_none.h`,
`thread_pthread.h` or `thread_win32.h` according to the
`tool/m4/ruby_thread.m4` creating the `THREAD_IMPL_H`.
The change in this commit only defining the `RB_THREAD_CURRENT_EC_NOINLINE` in
the `thread_pthread.h` is okay in this situation. Because in the
`thread_none.h` case, the thread feature is not used at all, including
Thread-Local Storage (TLS), and in the `thread_win32.h` case, the
`RB_THREAD_LOCAL_SPECIFIER` is not defined. In the `thread_pthread.h` case, the
`RB_THREAD_LOCAL_SPECIFIER` is defined in the `configure.ac`. In the
`thread_none.h` case, the `RB_THREAD_LOCAL_SPECIFIER` is defined in the
`thread_none.h`.
This commit fixes the failures in bootstraptest/test_ractor.rb reported on
the issue ticket <https://bugs.ruby-lang.org/issues/21534>.
TLS (Thread-Local Storage) may not be accessed across .so on ppc64le too.
I am not sure about that. The comment "// TLS can not be accessed across
.so on ..." in this commit comes from the following commit.
https://github.com/ruby/ruby/commit/319afed20fba8f9b44611d16e4930260f7b56b86#diff-408391c43b2372cfe1fefb3e1c2531df0184ed711f46d229b08964ec9e8fa8c7R118
> // on Darwin, TLS can not be accessed across .so`
This failures only happened when configuring with cppflags=-DRUBY_DEBUG and -O3
on ppc64le.
The reproducing steps were below.
```
$ ./autogen.sh
$ ./configure -C --disable-install-doc cppflags=-DRUBY_DEBUG
$ make -j4
$ make btest BTESTS=bootstraptest/test_ractor.rb
...
FAIL 2/147 tests failed
make: *** [uncommon.mk:913: yes-btest] Error 1
```
The steps with a reproducing script based on the `bootstraptest/test_ractor.rb`
were below.
```
$ cat test_ractor_1.rb
counts = []
counts << Ractor.count
p counts.inspect
ractors = (1..2).map { Ractor.new { Ractor.receive } }
counts << Ractor.count
p counts.inspect
ractors[0].send('End 0').join
sleep 0.1 until ractors[0].inspect =~ /terminated/
counts << Ractor.count
p counts.inspect
ractors[1].send('End 1').join
sleep 0.1 until ractors[1].inspect =~ /terminated/
counts << Ractor.count
p counts.inspect
$ make run TESTRUN_SCRIPT=test_ractor_1.rb
...
vm_core.h:2017: Assertion Failed: rb_current_execution_context:ec == rb_current_ec_noinline()
...
```
The assertion failure happened at the following line.
https://github.com/ruby/ruby/blob/f3206cc79bec2fd852e81ec56de59f0a67ab32b7/vm_core.h#L2017
This fix is similar with the following commit for the arm64.
https://github.com/ruby/ruby/commit/f7059af50a31a4d27a04ace0beadb60616f3f971
Fixes [Bug #21534]
* Added `Ractor::Port`
* `Ractor::Port#receive` (support multi-threads)
* `Rcator::Port#close`
* `Ractor::Port#closed?`
* Added some methods
* `Ractor#join`
* `Ractor#value`
* `Ractor#monitor`
* `Ractor#unmonitor`
* Removed some methods
* `Ractor#take`
* `Ractor.yield`
* Change the spec
* `Racotr.select`
You can wait for multiple sequences of messages with `Ractor::Port`.
```ruby
ports = 3.times.map{ Ractor::Port.new }
ports.map.with_index do |port, ri|
Ractor.new port,ri do |port, ri|
3.times{|i| port << "r#{ri}-#{i}"}
end
end
p ports.each{|port| pp 3.times.map{port.receive}}
```
In this example, we use 3 ports, and 3 Ractors send messages to them respectively.
We can receive a series of messages from each port.
You can use `Ractor#value` to get the last value of a Ractor's block:
```ruby
result = Ractor.new do
heavy_task()
end.value
```
You can wait for the termination of a Ractor with `Ractor#join` like this:
```ruby
Ractor.new do
some_task()
end.join
```
`#value` and `#join` are similar to `Thread#value` and `Thread#join`.
To implement `#join`, `Ractor#monitor` (and `Ractor#unmonitor`) is introduced.
This commit changes `Ractor.select()` method.
It now only accepts ports or Ractors, and returns when a port receives a message or a Ractor terminates.
We removes `Ractor.yield` and `Ractor#take` because:
* `Ractor::Port` supports most of similar use cases in a simpler manner.
* Removing them significantly simplifies the code.
We also change the internal thread scheduler code (thread_pthread.c):
* During barrier synchronization, we keep the `ractor_sched` lock to avoid deadlocks.
This lock is released by `rb_ractor_sched_barrier_end()`
which is called at the end of operations that require the barrier.
* fix potential deadlock issues by checking interrupts just before setting UBF.
https://bugs.ruby-lang.org/issues/21262
Many libraries should be loaded on the main ractor because of
setting constants with unshareable objects and so on.
This patch allows to call `requore` on non-main Ractors by
asking the main ractor to call `require` on it. The calling ractor
waits for the result of `require` from the main ractor.
If the `require` call failed with some reasons, an exception
objects will be deliverred from the main ractor to the calling ractor
if it is copy-able.
Same on `require_relative` and `require` by `autoload`.
Now `Ractor.new{pp obj}` works well (the first call of `pp` requires
`pp` library implicitly).
[Feature #20627]
This patch introduce M:N thread scheduler for Ractor system.
In general, M:N thread scheduler employs N native threads (OS threads)
to manage M user-level threads (Ruby threads in this case).
On the Ruby interpreter, 1 native thread is provided for 1 Ractor
and all Ruby threads are managed by the native thread.
From Ruby 1.9, the interpreter uses 1:1 thread scheduler which means
1 Ruby thread has 1 native thread. M:N scheduler change this strategy.
Because of compatibility issue (and stableness issue of the implementation)
main Ractor doesn't use M:N scheduler on default. On the other words,
threads on the main Ractor will be managed with 1:1 thread scheduler.
There are additional settings by environment variables:
`RUBY_MN_THREADS=1` enables M:N thread scheduler on the main ractor.
Note that non-main ractors use the M:N scheduler without this
configuration. With this configuration, single ractor applications
run threads on M:1 thread scheduler (green threads, user-level threads).
`RUBY_MAX_CPU=n` specifies maximum number of native threads for
M:N scheduler (default: 8).
This patch will be reverted soon if non-easy issues are found.
[Bug #19842]
Checking by `__STDC_VERSION__` is unreliable because old gcc 4.8
supports `-std=gnu11` option but does not implement `_Thread_local`.
Check the implementation directly instead.
Show native thread's serial on `RUBY_DEBUG_LOG`.
`nt->serial` is also stored into `ruby_nt_serial` if the compiler
supports `RB_THREAD_LOCAL_SPECIFIER`.
`NON_SCALAR_THREAD_ID` shows `pthread_t` is non-scalar (non-pointer)
and only s390x is known platform. However, the supporting code is
very complex and it is only used for deubg print information.
So this patch removes the support of `NON_SCALAR_THREAD_ID`
and make the code simple.
`rb_thread_t` contained `native_thread_data_t` to represent
thread implementation dependent data. This patch separates
them and rename it `rb_native_thread` and point it from
`rb_thraed_t`.
Now, 1 Ruby thread (`rb_thread_t`) has 1 native thread (`rb_native_thread`).
Now GVL is not process *Global* so this patch try to use
another words.
* `rb_global_vm_lock_t` -> `struct rb_thread_sched`
* `gvl->owner` -> `sched->running`
* `gvl->waitq` -> `sched->readyq`
* `rb_gvl_init` -> `rb_thread_sched_init`
* `gvl_destroy` -> `rb_thread_sched_destroy`
* `gvl_acquire` -> `thread_sched_to_running` # waiting -> ready -> running
* `gvl_release` -> `thread_sched_to_waiting` # running -> waiting
* `gvl_yield` -> `thread_sched_yield`
* `GVL_UNLOCK_BEGIN` -> `THREAD_BLOCKING_BEGIN`
* `GVL_UNLOCK_END` -> `THREAD_BLOCKING_END`
* removed
* `rb_ractor_gvl`
* `rb_vm_gvl_destroy` (not used)
There are GVL functions such as `rb_thread_call_without_gvl()` yet
but I don't have good name to replace them. Maybe GVL stands for
"Greate Valuable Lock" or something like that.
To make some kind of Ractor related extensions, some functions
should be exposed.
* include/ruby/thread_native.h
* rb_native_mutex_*
* rb_native_cond_*
* include/ruby/ractor.h
* RB_OBJ_SHAREABLE_P(obj)
* rb_ractor_shareable_p(obj)
* rb_ractor_std*()
* rb_cRactor
and rm ractor_pub.h
and rename srcdir/ractor.h to srcdir/ractor_core.h
(to avoid conflict with include/ruby/ractor.h)
To access TLS, it is faster to use language TLS specifier instead
of using pthread_get/setspecific functions.
Original proposal is: Use native thread locals. #3665
This commit introduces Ractor mechanism to run Ruby program in
parallel. See doc/ractor.md for more details about Ractor.
See ticket [Feature #17100] to see the implementation details
and discussions.
[Feature #17100]
This commit does not complete the implementation. You can find
many bugs on using Ractor. Also the specification will be changed
so that this feature is experimental. You will see a warning when
you make the first Ractor with `Ractor.new`.
I hope this feature can help programmers from thread-safety issues.
According to MSVC manual (*1), cl.exe can skip including a header file
when that:
- contains #pragma once, or
- starts with #ifndef, or
- starts with #if ! defined.
GCC has a similar trick (*2), but it acts more stricter (e. g. there
must be _no tokens_ outside of #ifndef...#endif).
Sun C lacked #pragma once for a looong time. Oracle Developer Studio
12.5 finally implemented it, but we cannot assume such recent version.
This changeset modifies header files so that each of them include
strictly one #ifndef...#endif. I believe this is the most portable way
to trigger compiler optimizations. [Bug #16770]
*1: https://docs.microsoft.com/en-us/cpp/preprocessor/once
*2: https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
`acquired' was an old boolean variable, but nowadays it is a
rb_thread_t pointer; "gvl.owner" seems like a more appropriate
name. And document the contended path including waitq, timer,
and timer_err.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@64581 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This matches the behavior of old timer thread more closely
and seems to fix [Bug #14999] when limited to a single CPU.
I cannot reproduce the error on a multi-core system unless
I use schedtool to force affinity to a single CPU:
schedtool -a 0x01 -e make test-spec \
MSPECOPT='-R1000 spec/ruby/library/conditionvariable/wait_spec.rb'
While it may be good enough to pass the spec, I don't have
huge degree of confidence in the interrupt handling robustness
under extremely heavy load (these may be ancient bugs, though).
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@64467 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Do not waste extra memory for each thread, but make
thread_pthread.c easier-to-follow as a result.
[ruby-core:88475] [Misc #14937]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@64375 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This reverts commit 194a6a2c68 (r64203).
Race conditions which caused the original reversion will be fixed
in the subsequent commit.
[ruby-core:88360] [Misc #14937]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@64352 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Maybe some platforms have strange condition variable implementations
which have a "memory" of which mutexes they're associated with.
In any case, it makes documentation easier even on GNU/Linux and
FreeBSD.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@64124 b2dd03c8-39d4-4d8f-98ff-823fe69b080e