80 Commits
Author SHA1 Message Date
John Hawthorn f609897fa4 Add Ractor#alive?
Matches Thread#alive? and Fiber#alive?. Returns false once the ractor's
result has been recorded. Reads sync.legacy under the ractor lock, as
Ractor#monitor does, so it cannot race with termination.

[Feature #22441]
2026-10-21 16:48:38 -07:00
Max Bernstein 9635fc0365 Don't allow writing to locals in cexpr (#18873)
The JIT optimizer sees an `invokebuiltin` and would like to assume that calling it does not write back to the frame. A `Primitive` should never write to the frame by taking a pointer to a local, so this PR removes that functionality.

Partial revert of https://github.com/ruby/ruby/commit/bb8924240d329091eaa38f46791036ac13ccd3ed.

----

* Don't allow writing to locals in cexpr

This makes the few remaining cases of `cexpr`-that-looks-at-locals a
read-only view.

* Remove uses of LOCAL_PTR from ractor.rb

Use two new builtin functions instead.

* Remove LOCAL_PTR from mk_builtin_loader

* Don't bother making the intermediate pointer

* Revert bb8924240d

* Add Primitive.local_self!
2026-09-23 16:36:21 -04:00
Luke Gruber 99e1c06692 Fix Ractor._activated: don't use Module#prepend
Use aliasing instead of Module#prepend with super to redefine `require`
when coming out of single ractor mode. There are bad interactions
between Module#prepend and aliasing and we have seen bugs related to
this in Rails CI: https://bugs.ruby-lang.org/issues/22263.

This should fix the issue we saw in Rails CI but doesn't fix the overall
design issue with Module#prepend and aliasing.

Define the `require` method on `Object` so that it acts like a prepend,
which is necessary if the redefined require (from Rubygems, Zeitwerk,
etc.) happens after we leave single-ractor mode. We don't want to run
Rubygems require in non-main Ractors because it has Ractor-unsafe code
and will error.
2026-09-17 11:51:24 -04:00
Koichi SasadaandClaude Opus 5 e3a070a490 Ractor.select: one port for the call, not one per watched ractor
select watched each Ractor through a Port it made for that ractor, and kept
a Hash from the port back to the ractor because a bare exit token did not
say which ractor it was about.  Making a port keys it into the ractor's
port table and closing it takes it out again, and keying it into the Hash
assigned it an object_id, which for a T_DATA lives in a side table that
takes its synchronized path as soon as a second ractor exists -- always,
here.  All of that was paid per watched ractor.

Now that a token names its ractor, one port serves the whole call: select
monitors every watched ractor on it and reads the ractor out of the token.
When nothing but ports were passed there is still no port to make, and when
nothing but ractors were passed there is a single port to wait on, so the
selector is not built at all.

Per call, in microseconds of CPU, with none of the watched ractors ever
terminating so that what is measured is building and tearing down the wait
set rather than waiting:

    watched     before      after
          0       0.61       0.61
          1       1.53       1.16
          4       3.65       1.68
         16      12.38       3.66
         64      45.21      11.73
        128      91.10      22.58
        256     187.99      44.49

Per watched ractor, 0.732us to 0.171us.  Three repetitions alternating
which build ran first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:19:13 +09:00
Koichi SasadaandClaude Opus 5 308677c703 Ractor#monitor: name the ractor in the exit token
A monitoring port received a bare :exited or :aborted, so a port could only
ever watch one ractor: with two, the token that arrived said what happened
but not to which.  Watching a group meant a port per ractor and a table
from the port back to the ractor -- which is what Ractor.select did, and
what anyone writing a supervisor had to do as well.

The token is now [ractor, :exited] or [ractor, :aborted], so one port can
watch a whole group and a supervisor needs nothing but monitor:

    workers.each { |r| r.monitor port }

    until workers.empty?
      r, status = port.receive
      workers.delete(r)
      workers << restart(r) if status == :aborted
    end

The pair is built by the receiver, from a basket type of its own, rather
than sent as a shareable array.  A shareable array would be simpler -- four
lines against thirty -- but it is left behind for every ractor that ever
terminates, and shareable objects are only reclaimed by a global
collection.  Creating 20,000 ractors and joining them:

                          wall       RSS held afterwards
    built on receipt     309 ms                    98 MB
    shareable array      570 ms                   533 MB

It cannot be sent as an ordinary copy either: the tokens go out from a
thread that has no VM stack left, which is what ractor_basket_new_ref
exists to avoid, and a copy pushes a tag.  Both halves are shareable
already, so the basket holds them as they are and the array is allocated
where it is read.

The array costs join, which received one symbol per call, 0.309us to
0.327us on a terminated ractor.  Ractor.select pays it back many times over
in the commit that follows.

This is an incompatible change to an experimental API, so NEWS says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:19:13 +09:00
Koichi SasadaandClaude Opus 5 acc5cd03d9 Ractor: widen the Ractor id to 64 bits
A Ractor id must stay unique for the life of the process, but the 32 bit
counter wraps after ~4e9 Ractors and comes back onto ids that are still in
use.  That is not out of reach: a few thousand Ractor.new/s is days, not
years.  The wrapped value is 0 too, which some callers read as "the main
Ractor".

There is no portable 64 bit atomic, so RUBY_ATOMIC_FETCH_ADD gives way to the
VM lock, which Ractor.new acquires a few frames later anyway.  That lock is a
no-op until there is a second Ractor, which is exactly when the GVL alone
already serializes the counter, so ractor_next_id asserts the same condition
vm_insert_ractor0 does.

rb_serial_t, to match ec->ractor_id and bmethod's defined_ractor_id.  Neither
struct grows: rb_ractor_pub had 4 bytes of padding after id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi SasadaandClaude Opus 5 9bfe1fcdb3 Ractor: wake a receiver when its port is closed
Ractor::Port#close left a receiver already waiting on that port asleep:
an untimed receive never returned, and a timed one waited out its whole
timeout and reported nil, where closing a port a thread waits on should
stop that thread.

Closing a port means nothing more can arrive on it, which is the kind of
news a send delivers by waking every waiter.  Do the same, with
wakeup_by_close -- the status already spelled out in the enum and
commented out.  The woken thread needs no new logic: ractor_close_port
deletes an emptied port from the table, and ractor_try_receive already
raises Ractor::ClosedError for a port that is no longer there.  A port
closed with messages still queued keeps them: it stays in the table until
a receiver drains it, and the last message takes it away.

Only a close that closed a port wakes anyone: ractor_queue_close reports
whether it was the call that closed the queue, so re-closing a closed
port is news to nobody.

Ractor.select over a port closed while it sleeps now raises
Ractor::ClosedError, as selecting an already-closed port does; it used to
sleep on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 04:07:37 +09:00
Koichi SasadaandClaude Fable 5 a9a3c2bbfa ractor: skip blocking_cnt bookkeeping on pthread
rb_ractor_blocking_threads_inc/dec took the VM lock on every blocking
region boundary of a ractor's last runnable thread -- for a 1-thread
ractor, twice per IO operation -- to maintain vm->ractor.blocking_cnt.
That counter is only consumed by the win32 scheduler, so the pthread
build now skips those VM-lock sections; the per-ractor
threads.blocking_cnt and the ractor creation/exit protocol stay.

Ractor#inspect's status becomes running/terminated only ("blocking" is
gone).  The finer states were really the win32 scheduler's
in-blocking-region flag leaking into inspect (under M:N, receive/sleep/
IO waits always showed "running" anyway, and the skip above stops the
remaining flips on pthread entirely).

R ractors x 1 thread, pipe write/read round-trips each, 16-HT machine
(Ryzen 9 5900HX), RUBY_MN_THREADS=1, best of 3, mean of 2 alternating
same-tree runs:

           before     after
     1R      342k      356k
     4R      877k     1016k
     8R      884k      976k
    16R      835k      884k
    24R      793k      839k

The remaining ceiling is the scheduler-lock contention addressed by
"thread: keep context switches off the scheduler lock"; combined, 16R
reaches 3.6M rt/s.

Suggested-by: Koichi Sasada <ko1@atdot.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 04:37:13 +09:00
Koichi Sasada d32793fe8e Ractor: let a wait for a message take a timeout
Ractor::Port#receive, Ractor.receive and Ractor.select gain a `timeout:`
keyword and return nil when it passes.

The wait itself stays where it is: rb_ractor_sched_wait() keeps parking the
thread in the thread scheduler, so an M:N thread still hands its native
thread back instead of becoming dedicated.  How the deadline is taken
depends on which kind of thread waits:

A dedicated native thread parks on its own condvar, so it takes the deadline
there, the way native_cond_sleep() does.  Nothing else is involved: the
condvar it waits on is the one a send already signals.

An M:N thread has no condvar of its own, so its deadline is armed on the
timer thread as a timeout-only wheel entry, and the timer thread wakes it
through thread_sched_to_ready_common(), which is exactly how
rb_ractor_sched_wakeup() wakes it for a send.  That gives such a waiter two
wakers, so rb_ractor_sched_wakeup() now takes an armed timeout back before
waking, skips a thread that a fired timeout already made runnable, and bumps
the scheduler event serial so a timeout that has not fired cannot wake it a
second time.

`timeout: 0` is a poll: it never blocks, converts nothing and reads no clock,
so it costs what a receive of a waiting message costs.  It still delivers the
incoming queue first, so a message another thread just sent is not missed.

A timeout that expires leaves the waiter on the Ractor's waiter list, as a
spurious wakeup does, and ractor_wait_receive() still delivers pending
messages before it reports the timeout, so a message that arrived just before
the deadline is not lost.

Both kinds of wait exist on every pthread platform, including builds without
the timer wheel (USE_MN_THREADS == 0), where every thread is dedicated.  On
win32 the wait is a condvar wait, which takes the timeout directly.
2026-08-22 00:07:09 +09:00
Koichi SasadaandClaude Fable 5 78ec25efa2 Ractor#value: return the value only once
Ractor#value used to be repeatable: the successor could call it again
and get the same object back, because the value stayed reachable from
the dead Ractor's C struct (sync.legacy).  Keeping that slot valid
after the Ractor is gone is what forced the successor to root and pin
every Ractor it absorbed, through a VM-wide list guarded by its own
lock, with add, unlink and scan coming from three different threads.

Take the value out of the struct on the first call and raise
Ractor::Error afterwards.  The value is then rooted by the caller like
any other return value, so the list, its lock and the pinning go away.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
himura467 d3aee406de Raise TypeError for uninitialized Ractor::Port
Ractor::Port has a public allocator, so Ractor::Port.allocate returns a port whose owner Ractor is still NULL; only ractor_port_init() fills it in. The methods dereferenced it unchecked:

Ractor::Port.allocate.closed?  # [BUG] Segmentation fault at 0x60

Route the paths reachable from Ruby through ractor_port_ptr_check(), which raises TypeError as other uninitialized typed data objects do. The self of #initialize_copy keeps the unchecked accessor because it is legitimately uninitialized there; only orig is checked.

Ractor.select did not crash, but such a port has id 0, the same as the current Ractor's default port, so it took messages from it.

Port#closed? also loses Primitive.attr! :leaf. It can raise now, and it was not a leaf before either, since the foreign Ractor path takes RACTOR_LOCK().

[Bug #22214]
2026-07-28 12:31:24 -04:00
John Hawthorn f289b0d688 Specify attr! leaf on some ractor.rb functions
These are all simple getters which don't allocate or raise exceptions.
We can declare them as a leaf and skip pushing frames inside YJIT/ZJIT.
2026-07-02 13:04:38 -07:00
Kazuhiro NISHIYAMA 0e4dbb2d5d [DOC] Fix the return value of Ractor#monitor
The RDoc `call-seq` for `Ractor#monitor` says it returns `self`, but the
method actually returns `true`/`false`.

It is implemented by `ractor_monitor()` in `ractor_sync.c`, which returns
only `Qtrue` or `Qfalse` — there is no code path that returns `self`:

- `true`  — the monitor was registered (the ractor is still running)
- `false` — the ractor had already terminated, so it was not registered;
  the termination message (`:exited` / `:aborted`) is sent to the port
  immediately

(For comparison, `Ractor#unmonitor` does `return self`, so its existing
`-> self` call-seq is correct and is left unchanged.)

This patch corrects the `call-seq` to `-> true or false` and documents the
meaning of the two return values.
2026-06-24 11:37:46 +09:00
Nery Campusano be8f647bda [Bug #21551] changing the exception to be isolationerror rather than argument error 2026-02-13 11:01:50 +09:00
Steve Savio bff7c6d9e9 [DOC] Fix minor typo on shareable procs section 2026-01-06 18:30:33 +09:00
Luke Gruber 481f16f3f1 [DOC] Improve ractor class docs (grammar, code examples) (#15686) 2025-12-22 13:22:01 -05:00
John Hawthorn 0c4fcdff32 Update ArgumentError message for Ractor.select 2025-12-18 16:47:35 -08:00
John Hawthorn 535233c68c [DOC] Update ractor.rb docs 2025-12-18 15:59:49 -08:00
Luke Gruber a7eb1879ad [DOC] small improvements to ractor class docs (#15584)
* Ractor.yield no longer exists
* Ractor.shareable_proc returns a copy of the given proc
* Improve wording for monitoring/unmonitoring ports
2025-12-18 15:08:36 -05:00
John Hawthorn 5828872ec4 Update Ractor warning message
Although the Ractor API is still experimental and may change, and there
may be some implementation issues, we should no longer say that there
are many.

Hopefully we can remove this warning entirely for Ruby 4.1
2025-12-10 16:01:27 -08:00
Koichi Sasada 2aaea665bb fix typo s/sharable/shareable/ 2025-12-05 03:10:50 +09:00
Koichi Sasada f8231dacd5 Ractor.store_if_absent should not warn
```ruby
$VERBOSE = true

Ractor.store_if_absent :key do
end #=> warning: the block passed to 'Ractor.store_if_absent' defined at <internal:ractor>:474 may be ignored
```
2025-12-04 17:15:42 +09:00
Koichi Sasada 55b1ba3bf2 Ractor.shareable_proc
call-seq:
  Ractor.sharable_proc(self: nil){} -> sharable proc

It returns shareable Proc object. The Proc object is
shareable and the self in a block will be replaced with
the value passed via `self:` keyword.

In a shareable Proc, the outer variables should
* (1) refer shareable objects
* (2) be not be overwritten

```ruby
  a = 42
  Ractor.shareable_proc{ p a }
  #=> OK

  b = 43
  Ractor.shareable_proc{ p b; b = 44 }
  #=> Ractor::IsolationError because 'b' is reassigned in the block.

  c = 44
  Ractor.shareable_proc{ p c }
  #=> Ractor::IsolationError because 'c' will be reassigned outside of the block.
  c = 45

  d = 45
  d = 46 if cond
  Ractor.shareable_proc{ p d }
  #=> Ractor::IsolationError because 'd' was reassigned outside of the block.
```

The last `d`'s case can be relaxed in a future version.

The above check will be done in a static analysis at compile time,
so the reflection feature such as `Binding#local_varaible_set`
can not be detected.

```ruby
  e = 42
  shpr = Ractor.shareable_proc{ p e } #=> OK
  binding.local_variable_set(:e, 43)
  shpr.call #=> 42 (returns captured timing value)
```

Ractor.sharaeble_lambda is also introduced.
[Feature #21550]
[Feature #21557]
2025-09-24 03:59:03 +09:00
Koichi Sasada f9bffff3d4 remove Ractor#take
[Feature #21262]
2025-09-01 17:34:41 +09:00
Nobuyoshi Nakada 6179cc0118 [DOC] Fill undocumented documents 2025-08-04 02:23:43 +09:00
Koichi Sasada 84253ce38c fix obsolete doc with Ractor::Port 2025-07-17 12:13:33 +09:00
zzak e8094943a4 s/sned/send 2025-06-09 16:23:08 +09:00
Koichi Sasada 8070d5d97d Ractor#take and warn
`Ractor#take` was deprecated but some libraries can use it as
an alias for `Ractor#value` (i.e., to wait for a Ractor's
temrination and retrieve its result).
Therefore `Ractor#take` is simply an alias for `Ractor#value`.

This method will remain available until the end of August 2025,
unless there is further discussion.
2025-06-04 19:18:16 +09: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
lukeg c941fced21 Throw RuntimeError if getting/setting ractor local storage for non-main ractor
[Bug #19367]
2025-05-13 13:18:10 +02:00
Kanstantsin Shautsou 4646ab8917 Use correct warn method 2025-04-07 16:37:39 +02:00
Lorenzo Zabot a203603be0 Correct typo in Ractor comment 2025-03-31 09:16:18 +09:00
Nobuyoshi Nakada e433e6515e [DOC] Exclude 'Class' and 'Module' from RDoc's autolinking 2025-01-02 12:36:06 +09:00
Nobuyoshi Nakada bf878b5494 [DOC] Ractor::RemoteError 2024-12-25 13:16:28 +09:00
Victor Shepelev efe671f9d3 Properly document Ractor#require (#12389) 2024-12-21 20:05:26 +02:00
Jean Boussier 5d97c14fec FIx Ractor.main? to return true not 0
[Bug #20954]
2024-12-16 08:37:55 +01:00
Koichi Sasada e09c23433e followup 0bdb38ba6b
(forgot to amend...)
2024-12-13 17:05:58 +09:00
Koichi Sasada 0bdb38ba6b Ractor.set_if_absent(key)
to initialize ractor local storage in thread-safety.
[Feature #20875]
2024-12-13 06:22:13 +09: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
Koichi Sasada 075a102c93 Ractor.[] and Ractor.[]=
`Ractor#[]/[]=` is only for accessors to the current ractor, so that
`Ractor.[]/[]=` is simpler.
[Feature #20715]
2024-11-08 18:02:46 +09:00
Koichi Sasada f0d0c030c0 Ractor.main?
to return the current ractor is the main ractor.
(== `Ractor.current == Ractor.main`)
2024-11-08 18:02:46 +09:00
Koichi Sasada c9a9b8036c remove Ractor::Selector from Ruby level
`Ractor::Selector` is not approved by Matz so remove it from
Ruby-level.

The implementation is used by `Ractor.select` so most of implementation
was remaind and calling `rb_init_ractor_selector()`, `Ractor::Selector`
will be defined. I will provide `ractor-selector` gem to try it.
2023-12-16 01:00:01 +09:00
Koichi Sasada 5875fce6ce Ractor::Selector#empty?
It returns the waiting set is empty or not.

Also add Ractor::Selector's tests.
2023-03-03 00:08:02 +09:00
Koichi Sasada a4421bd73c Rewrite Ractor synchronization mechanism
This patch rewrites Ractor synchronization mechanism, send/receive
and take/yield.

* API
  * Ractor::Selector is introduced for lightweight waiting
    for many ractors.
* Data structure
  * remove `struct rb_ractor_waiting_list` and use
    `struct rb_ractor_queue takers_queue` to manage takers.
  * remove `rb_ractor_t::yield_atexit` and use
    `rb_ractor_t::sync::will_basket::type` to check the will.
  * add `rb_ractor_basket::p.take` to represent a taking ractor.
* Synchronization protocol
  * For the Ractor local GC, `take` can not make a copy object
    directly so ask to generate the copy from the yielding ractor.
  * The following steps shows what `r1.take` does on `r0`.
    * step1: (r0) register `r0` into `r1`'s takers.
    * step2: (r0) check `r1`'s status and wakeup r0 if `r1` is waiting
             for yielding a value.
    * step3: (r0) sleep until `r1` wakes up `r0`.
  * The following steps shows what `Ractor.yield(v)` on `r1`.
    * step1: (r1) check first takers of `r1` and if there is (`r0`),
             make a copy object of `v` and pass it to `r0` and
             wakes up `r0`.
    * step2: (r1) if there is no taker ractors, sleep until
             another ractor try to take.
2023-03-02 14:31:54 +09:00
John Bampton 2f7270c681 Fix spelling (#7389) 2023-02-27 09:56:06 -08:00
Luke Gruber 4ffef59bb1 [DOC] Make changes to docs in ractor.rb (#7180)
* Make changes to docs in ractor.rb

Mainly English changes to make things more clear, and to fix minor
non-idiomatic phrases. Also clarified difference between frozen and
shareable objects.

* More minor changes to Ractor docs.
2023-01-29 10:01:49 +09:00
Nobuyoshi Nakada 131c31a920 [Bug #19081] Show the caller location in warning for Ractor
The internal location in ractor.rb is not usefull at all.
```
$ ruby -e 'Ractor.new {}'
<internal:ractor>:267: warning: Ractor is experimental, ...
```
2022-10-26 19:43:14 +09:00
Felix Yan 1486ffe039 [DOC] Correct article of Ractor's introduction [ci skip] 2022-08-28 16:27:11 +09:00
Nobuyoshi Nakada 94c3d528e7 Fix conversion of rb_ractor_id() 2022-07-28 23:46:07 +09:00
Nobuyoshi Nakada a27c274f04 [DOC] Fix broken links [ci skip]
* As the "doc/" prefix is specified by the `--page-dir` option,
  remove from the rdoc references.
* Refer to the original .rdoc instead of the converted .html.
2021-09-15 14:16:14 +09:00