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]
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!
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.
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>
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>
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>
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>
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>
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.
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>
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]
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.
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
```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
```
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]
`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.
* 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]
`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.
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.
* 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.
The internal location in ractor.rb is not usefull at all.
```
$ ruby -e 'Ractor.new {}'
<internal:ractor>:267: warning: Ractor is experimental, ...
```
* 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.