546 Commits

Author SHA1 Message Date
Peter Zhu
d53ddbe2a1 Make rb_internal_thread_event_hooks_rw_lock_atfork static 2026-07-30 14:53:52 -04:00
Koichi Sasada
6a098a07e5 Assert the serial-epilogue ordering invariants
Encode the safety argument of the epilogue reorder as VM_ASSERTs: a
non-last dying thread still owns its scheduler slot when it leaves the
living set (its VM-lock work stays a counted running thread), and the
last thread's reverse order is only safe because it has no successor
(running == NULL, so the removal cannot join a barrier). Also check
winding_cnt against underflow at the reclaim decrement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:32:41 +09:00
Koichi Sasada
c2992bbd7a Reset the Ractor barrier state in the child after fork
A fork taken while the parent holds the VM barrier (e.g. rb_gc_before_fork)
leaves the child with barrier_waiting set and a barrier owner that no longer
exists, so the next barrier in the child could never complete. Reset the
barrier fields in thread_sched_atfork like the rest of the scheduler state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:32:41 +09:00
Koichi Sasada
d6464c280a Count every Ractor barrier joiner again
The is_running check added by 3fa1c8708 guarded against a terminating
thread joining the barrier after leaving the running set.  Since the
previous commit such a thread leaves the living set before handing over
its scheduler slot, so every joiner is a running-set member again.
Restore the unconditional count and assert the invariant instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:32:41 +09:00
Koichi Sasada
3fa1c87089 Do not count a not-yet-running thread as a Ractor barrier waiter
`rb_ractor_sched_barrier_join` unconditionally did `barrier_waiting_cnt++`.
A thread can reach it (via `vm_lock_enter`) while its Ractor's
`sched.running` is set but the thread is not yet in the VM running set
(`sched.is_running == false`) -- e.g. resuming from a blocking IO wait.
Counting such a thread pushes `barrier_waiting_cnt` past `running_cnt - 1`
and trips `VM_ASSERT(running_cnt - 1 >= barrier_waiting_cnt)` in
`ractor_sched_barrier_completed_p`; in release builds the unsigned
`(running_cnt - barrier_waiting_cnt) == 1` completion test can underflow.

Increment `barrier_waiting_cnt` only when `cr->threads.sched.is_running`
(O(1), toggled together with running_threads add/del).  A not-running
thread is parked at the barrier regardless, so it is safe to wait without
counting.

NOTE (open): this guards the count at the join site.  A stricter fix would
prevent a not-running thread from reaching barrier_join at all; that needs
care so it still waits for the barrier rather than proceeding during
compaction.  Left for review.

Reproduction is currently coupled with a separate pre-existing compaction
use-after-poison on a blocked-IO thread's STR_TMPLOCK buffer (only the
IO-read blocking path triggers this barrier window), so a standalone
passing test awaits that fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:03:53 +09:00
copilot-swe-agent[bot]
d939495a8f Fix unused variable warning in thread_sched_set_running on non-DTrace platforms 2026-07-09 13:27:35 -07:00
Koichi Sasada
bcd9129c21 MN threads: reclaim a terminated coroutine via nt->dead_co, not the transfer return value
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>
2026-07-10 01:35:36 +09:00
Koichi Sasada
880cc0a274 thread_pthread.c: reset grq_cnt at fork
thread_sched_atfork empties the global ready queue with
ccan_list_head_init but leaves vm->ractor.sched.grq_cnt at whatever the
parent had, so a child forked while any Ractor was enqueued keeps a stale
non-zero count for an empty list. VM_CHECK_MODE builds then fail
"grq_size(vm, cr) == vm->ractor.sched.grq_cnt" in ractor_sched_deq
(reproducible on master with RUBY_MN_THREADS=1: fork in a loop that also
creates Ractors); on regular builds the counter only feeds assertions and
debug logs, so the corruption stays silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 04:41:36 +09:00
Koichi Sasada
31b41ac5fb MN threads: a dying coroutine thread always returns to its native thread
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>
2026-07-09 04:41:36 +09:00
Koichi Sasada
6acb0fb49e thread_pthread.c: cancel a Ractor's ready-queue entry when served directly
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>
2026-07-09 04:41:36 +09:00
Kunshan Wang
89e7b08a9f
eBPF-based timeline visualization tool (#17190) 2026-07-07 19:07:22 +09:00
Koichi Sasada
051c9db646 coroutine (amd64): annotate fiber switches for ThreadSanitizer
ThreadSanitizer cannot follow Ruby's userspace coroutine stack switches, so
its per-thread shadow stack leaks across a switch and eventually faults
inside libtsan. Create a TSan fiber per coroutine and switch to it at each
transfer, mirroring the existing AddressSanitizer annotations. The main
context borrows the OS thread's implicit fiber and must not destroy it.

Only amd64 is annotated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 06:56:19 +09:00
Samuel Williams
d58f02910c
Use page-sized stack overflow detection window. (#17436)
* Remove stack overflow space ratio

* Cache page size.
2026-06-24 09:44:17 +12:00
Samuel Williams
c7190d3b08
Remove unused native thread stack size. (#17435) 2026-06-23 18:17:16 +12:00
Hiroshi SHIBATA
25c1df0a95 Fix stale runnable_hot_th hint causing assertion failure with MN threads
The hot thread cannot steal back control when sched->running is an MN
thread, and goes to sleep with sched->runnable_hot_th still pointing to
itself, firing VM_ASSERT(sched->runnable_hot_th != th) at the next
wakeup. Drop the hint before sleeping since the thread is no longer
spinning. On a VM_CHECK_MODE build with RUBY_MN_THREADS=1:

    Thread.new { loop {} }
    100.times { File.read(IO::NULL) }

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 11:49:31 +09:00
thekuwayama
e98f95b4fd
Use nprocessors as default_max_cpu for M:N scheduler (#17100)
Resolves the "TODO: CPU num?". Uses `sysconf(_SC_NPROCESSORS_ONLN)` with
a fallback to 8, guarded by `#if defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)`.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 19:09:12 +09:00
Jacob Lacouture
5796bc37d5 use a hint avoid the optimistic sched lock drop/reacquire 2026-03-26 11:58:12 +09:00
Jacob Lacouture
ddb8d353ac Give the hot thread scheduler priority 2026-03-26 11:58:12 +09:00
Luke Gruber
fa20111333 Fix ubf_ractor_wait
There was a race between ubf_ractor_wait and `rb_ractor_sched_wakeup` (Port#send,Ractor#send).

It could occur with a sequence like this:

1) r1 t1: r2#send (rb_ractor_sched_wakeup(r2, t2))
2) ubf called for r2, but the caller gets descheduled right before acquiring RACTOR_LOCK(r2)
   in ubf_ractor_wait
3) r2 t2 wakes up from #send call, takes RACTOR_LOCK, clears ubf, waiter is
   no longer on its stack.
4) ubf resumes, acquires RACTOR_LOCK(r2), tries to touch waiter
   (use_after_return)

This was caught by ASAN:
https://ci.rvm.jp/results/trunk_asan@ruby-sp1/6247788
2026-03-23 13:56:49 -04:00
Andrey Marchenko
c8155822c4 reinit rb_internal_thread_event_hooks_rw_lock at fork
[Bug #21959]
2026-03-20 17:21:05 +01:00
Luke Gruber
d72a0fed60 Always take th->interrupt_lock in ubf_clear
Patch 08372635f7 fixed a race condition on ubfs, but it's only valid if right after
a call to `ubf_clear`, we assume the ubf function cannot be in the middle of running.
This patch removes an optimization in `ubf_clear` that violates that assumption. In short,
`ubf_clear` needs to take `th->interrupt_lock` unconditionally both to avoid deadlocks and to be
able to reason about when ubfs can be run.

This should fix CI errors like https://ci.rvm.jp/results/trunk-jemalloc@ruby-sp2-noble-docker/6242153.
The error was in test_timeout.rb, which had a deadlock during VM shutdown.

```ruby
r = Ractor.new do
begin
    Timeout.timeout(0.1) { sleep }
rescue Timeout::Error
    :ok
end
end.value

assert_equal :ok, r
```

The deadlock happened during `rb_ractor_terminate_interrupt_main_thread` with 2 ractors:

1) r1 t1: UBF called with t2->interrupt_lock (ubf = ubf_waiting)
2) r2 t2: ubf cleared from previous thread_sched_wait_events_call (but no lock taken, because of optimization)
3) r2 t2: thread_sched_wait_events: acquire thread_sched_lock(t2) (caller calling native_sleep() in loop)
4) r2 t2: ubf_set: try to acquire t2->interrupt_lock [block]
5) r1 t1: try to acquire thread_sched_lock(t2) [block, deadlock]

t2 needs to block on t2->interrupt_lock in step 2 until the ubf has completed. Only then can it register a new
ubf in the next `native_sleep` iteration.
2026-03-11 09:24:18 -04:00
Luke Gruber
08372635f7 Fix race condition right after ubf registration
Registering a ubf was considered problematic in some cases because it could
result in lock ordering inversions with the ubf function itself. I believe
this is the reason that in patch be1bbd5b7d, the ubf was registered outside of
the `thread_sched_lock`.

For example, `thread_sched_to_waiting_until_wakeup()` (native_sleep)
should register the ubf with the thread_sched_lock (TSL) taken. The ordering
is (TSL -> UBF lock). During the ubf call itself, since the UBF lock is
always acquired during the call, the ordering is (UBF lock -> TSL). This
inversion was avoided by registering the ubf outside the TSL.

This inversion is benign in this case, though. Since there can be no ubf on the
thread before the call to `ubf_set`, and this ubf is the only place where the
(UBF lock -> TSL lock) ordering is done, it is okay to register it with the TSL.
In fact registering the ubf outside the TSL can result in problamatic race conditions,
so we now register the ubf with the TSL lock.

This fixes a race condition whereby a newly registered ubf is called before the thing it's
protecting (the sleep) is actually run. The sleeping thread can never be interrupted if this
happens.

Fixes [Bug #21926]
2026-03-09 15:25:08 -04:00
John Hawthorn
fd089e3e95 Revert "Wake timer to create new SNT when needed for dedicated task (#16009)"
This reverts commit 7b3370a5579956404d742a2e104d72e7c89480e4.
2026-02-07 18:20:17 -08:00
Luke Gruber
7b3370a557
Wake timer to create new SNT when needed for dedicated task (#16009)
When removing a thread from `running_threads`, if we're on a shared
native thread and we're running a dedicated task, we need to wake
the timer thread so it can create a new SNT if necessary. We only
do this if it's waiting forever without the 10ms quantum timeout
for now, because max 10ms of wait is considered "good enough".
In the future, perhaps we can force the timer thread to wake if this
becomes an issue (`timer_thread_wakeup_force`).

Fixes [Bug #21504]
2026-02-06 14:26:07 -05:00
Jean Boussier
32f2596984 thread_pthread.c: Use ruby_sized_xfree 2026-02-01 15:32:28 +01:00
Chris Hasiński
5add7c3ea9
Fix RUBY_MN_THREADS sleep returning prematurely (#15868)
timer_thread_check_exceed() was returning true when the remaining time
was less than 1ms, treating it as "too short time". This caused
sub-millisecond sleeps (like sleep(0.0001)) to return immediately
instead of actually sleeping.

The fix removes this optimization that was incorrectly short-circuiting
short sleep durations. Now the timeout is only considered exceeded when
the actual deadline has passed.

Note: There's still a separate performance issue where MN_THREADS mode
is slower for sub-millisecond sleeps due to the timer thread using
millisecond-resolution polling. This will require a separate fix to
use sub-millisecond timeouts in kqueue/epoll.

[Bug #21836]
2026-01-26 10:17:35 -05:00
Luke Gruber
7e81bf5c0c
Fix sleep spurious wakeup from sigchld (#15802)
When sleeping with `sleep`, currently the main thread can get woken up from sigchld
from any thread (subprocess exited). The timer thread wakes up the main thread when this
happens, as it checks for signals. The main thread then executes the ruby sigchld handler
if one is registered and is supposed to go back to sleep immediately. This is not ideal but
it's the way it's worked for a while. In commit 8d8159e7d8 I added writes to `th->status`
before and after `wait_running_turn` in `thread_sched_to_waiting_until_wakeup`, which is
called from `sleep`. This is usually the right way to set the thread's status, but `sleep`
is an exception because the writes to `th->status` are done in `sleep_forever`. There's a
loop that checks `th->status` in `sleep_forever`. When the main thread got woken up from
sigchld it saw the changed `th->status` and continued to run the main thread instead of
going back to sleep.

The following script shows the error. It was returning instead of sleeping forever.

```ruby
t = Thread.new do
  sleep 0.3
  `echo hello`  # Spawns subprocess
  puts "Subprocess exited"
end

puts "Main thread sleeping..."
result = sleep  # Should block forever
puts "sleep returned: #{result.inspect}"
```

Fixes [Bug #21812]
2026-01-05 12:18:47 -05:00
Luke Gruber
7909ce2a83
move th->event_serial to rb_thread_sched_item (#15500) 2025-12-12 14:24:40 -05:00
Koichi Sasada
d428d086c2 Simplify the code
`thread_sched_to_waiting_common0` is no longer needed.
2025-12-12 18:46:06 +09:00
Yuji Teshima
264c469bc2
Fix typo in thread_pthread.c [ci skip] (#15465)
Fix typo in thread_pthread.c: threre -> there
2025-12-10 08:28:18 +09:00
Luke Gruber
8d8159e7d8
Fix thread scheduler issue with thread_sched_wait_events (#15392)
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)
```
2025-12-04 16:51:11 -05:00
Luke Gruber
d2c30a3bae
Fix thread_sched_wait_events race (#15067)
This race condition was found when calling `Thread#join` with a timeout
inside a ractor. The race is between the polling thread waking up the
thread and the `ubf` getting called (`ubf_event_waiting`). The error was
that the ubf or polling thread would set the thread as ready, but then
the other function would do the same.

Fixes [Bug #21614]
2025-11-10 20:32:30 -05:00
Andre Muta
0531fa4d6f mn timer thread: force wakeups for timeouts 2025-10-30 13:44:25 -07:00
Luke Gruber
446257c84b Add debug #define to call sched_yield before each pthread_mutex_lock
This is useful for debugging mutex issues as it increases contention for locks.
It is off by default.
2025-10-07 13:00:16 -07:00
Peter Zhu
d8c8623f50 Set context_stack on main thread
We allocate the stack of the main thread using malloc, but we never set
malloc_stack to true and context_stack. If we fork, the main thread may
no longer be the main thread anymore so it reports memory being leaked
in RUBY_FREE_AT_EXIT.

This commit allows the main thread to free its own VM stack at shutdown.
2025-09-30 09:47:29 -04:00
Luke Gruber
07878ebe78 Fix lock ordering issue for rb_ractor_sched_wait() and rb_ractor_sched_wakeup()
In rb_ractor_sched_wait() (ex: Ractor.receive), we acquire
RACTOR_LOCK(cr) and then thread_sched_lock(cur_th). However, on wakeup
if we're a dnt, in thread_sched_wait_running_turn() we acquire
thread_sched_lock(cur_th) after condvar wakeup and then RACTOR_LOCK(cr).
This lock inversion can cause a deadlock with rb_ractor_wakeup_all()
(ex: port.send(obj)), where we acquire RACTOR_LOCK(other_r) and then
thread_sched_lock(other_th).

So, the error happens:

nt 1:   Ractor.receive
            rb_ractor_sched_wait() after condvar wakeup in thread_sched_wait_running_turn():
              - thread_sched_lock(cur_th) (condvar) # acquires lock
              - rb_ractor_lock_self(cr) # deadlock here: tries to acquire, HANGS

nt 2: port.send
            ractor_wakeup_all()
              - RACTOR_LOCK(port_r) # acquires lock
              - thread_sched_lock # tries to acquire, HANGS

To fix it, we now unlock the thread_sched_lock before acquiring the
ractor_lock in rb_ractor_sched_wait().

Script that reproduces issue:

```ruby
require "async"
class RactorWrapper
  def initialize
    @ractor = Ractor.new do
      Ractor.recv # Ractor doesn't start until explicitly told to
      # Do some calculations
      fib = ->(x) { x < 2 ? 1 : fib.call(x - 1) + fib.call(x - 2) }
      fib.call(20)
    end
  end

  def take_async
    @ractor.send(nil)
    Thread.new { @ractor.value }.value
  end
end

Async do |task|
  10_000.times do |i|
    task.async do
      RactorWrapper.new.take_async
      puts i
    end
  end
end
exit 0
```

Fixes [Bug #21398]

Co-authored-by: John Hawthorn <john.hawthorn@shopify.com>
2025-08-08 13:37:31 -07:00
Peter Zhu
6e36841dbd Free rb_native_thread memory at fork
We never freed any resources of rb_native_thread at fork because it would
cause it to hang. This is because it called rb_native_cond_destroy for
condition variables.  We can't call rb_native_cond_destroy here because
according to the specs of pthread_cond_destroy:

    Attempting to destroy a condition variable upon which other threads
    are currently blocked results in undefined behavior.

Specifically, glibc's pthread_cond_destroy waits on all the other listeners.
Since after forking all the threads are dead, the condition variable's
listeners will never wake up, so it will hang forever.

This commit changes it to only free the memory and none of the condition
variables.
2025-06-12 15:23:50 -04:00
Koichi Sasada
ef2bb61018 Ractor::Port
* 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
2025-05-31 04:01:33 +09:00
Nobuyoshi Nakada
aad9fa2853
Use RB_VM_LOCKING 2025-05-25 15:22:43 +09:00
John Hawthorn
d67d169aea Use atomics for system_working global
Although it almost certainly works in this case, volatile is best not
used for multi-threaded code. Using atomics instead avoids warnings from
TSan.

This also simplifies some logic, as system_working was previously only
ever assigned to 1, so --system_working <= 0 should always return true
(unless it underflowed).
2025-05-15 15:18:10 -07:00
John Hawthorn
d845da05e8 Force reset running time in timer interrupt
Co-authored-by: Ivo Anjo <ivo.anjo@datadoghq.com>
Co-authored-by: Luke Gruber <luke.gru@gmail.com>
2025-05-15 14:44:26 -07:00
Luke Gruber
1d4822a175 Get ractor message passing working with > 1 thread sending/receiving values in same ractor
Rework ractors so that any ractor action (Ractor.receive, Ractor#send, Ractor.yield, Ractor#take,
Ractor.select) will operate on the thread that called the action. It will put that thread to sleep if
it's a blocking function and it needs to put it to sleep, and the awakening action (Ractor.yield,
Ractor#send) will wake up the blocked thread.

Before this change every blocking ractor action was associated with the ractor struct and its fields.
If a ractor called Ractor.receive, its wait status was wait_receiving, and when another ractor calls
r.send on it, it will look for that status in the ractor struct fields and wake it up. The problem was that
what if 2 threads call blocking ractor actions in the same ractor. Imagine if 1 thread has called Ractor.receive
and another r.take. Then, when a different ractor calls r.send on it, it doesn't know which ruby thread is associated
to which ractor action, so what ruby thread should it schedule? This change moves some fields onto the ruby thread
itself so that ruby threads are the ones that have ractor blocking statuses, and threads are then specifically scheduled
when unblocked.

Fixes [#17624]
Fixes [#21037]
2025-05-13 13:23:57 -07:00
Nobuyoshi Nakada
c218862d3c
Fix style [ci skip] 2025-04-19 22:02:10 +09:00
Samuel Williams
c4ae6cb500
Prefer th->ec for stack base/size. (#13101) 2025-04-17 13:21:51 +00:00
John Hawthorn
8f952a1178 Clear VM_CHECK lock info on fork
We are resetting the actual lock so we should reset this information at
the same time. Previously this caused an assertion to fail in debug
mode.
2025-03-25 19:14:26 -07:00
Nobuyoshi Nakada
4a67ef09cc
[Feature #21116] Extract RJIT as a third-party gem 2025-02-13 18:01:03 +09:00
Luke Gruber
2a08f7283e Fix [Bug #20779] Dedicated native thread creation failed bug
When a dedicated native thread fails to create (OS can't create more
threads), don't add the ruby-level thread to the thread scheduler. If
it's added, then next time the thread is scheduled to run it will sleep
forever, waiting for a native thread that doesn't exist to pick it up.
2024-12-24 09:05:49 +09:00
Aaron Patterson
fffef9aa5d Add an environment variable for controlling the default Thread quantum
This commit adds an environment variable `RUBY_THREAD_TIMESLICE` for
specifying the default thread quantum in milliseconds.  You can adjust
this variable to tune throughput, which is especially useful on
multithreaded systems that are mixing CPU bound work and IO bound work.

The default quantum remains 100ms.

[Feature #20861]

Co-Authored-By: John Hawthorn <john@hawthorn.email>
2024-12-12 16:04:49 -08:00
Koichi Sasada
aa63699d10 support require in non-main Ractors
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]
2024-11-08 18:02:46 +09:00
KJ Tsanaktsidis
dcf3add96b Delete reserve_stack code
This code was working around a bug in the Linux kernel. It was
previously possible for the kernel to place heap pages in a region where
the stack was allowed to grow into, and then therefore run out of usable
stack memory before RLIMIT_STACK was reached.

This bug was fixed in Linux commit
c204d21f22
for kernel 4.13 in 2017. Therefore, in 2024, we should be safe to delete
this workaround.

[Bug #20804]
2024-10-22 17:27:20 +11:00