mirror of
https://github.com/ruby/ruby.git
synced 2026-11-08 01:20:26 +08:00
Ractor.new: use Ractor.shareable_proc semantics for the block [Feature #22444]
Previously Ractor.new isolated the given block Proc in place (rb_proc_isolate_bang) when the ractor's thread was set up. This mutated the caller's Proc, and rejected every outer variable access. Now Ractor.new duplicates the block and makes the copy shareable exactly as Ractor.shareable_proc does, before any ractor resources are allocated. The block may read outer variables that are never reassigned and refer to shareable objects; writes, reassigned variables, unshareable values and yield still raise Ractor::IsolationError. The original Proc is left unchanged (not frozen, not shareable), and instance variables on it no longer prevent use, as the copy does not carry them. Procs without an iseq (e.g. method procs) keep using rb_proc_isolate. Tests and docs are updated accordingly.
This commit is contained in:
parent
70c8791f42
commit
1f3e810694
Notes:
git
2026-11-04 15:14:08 +00:00
6
NEWS.md
6
NEWS.md
@ -157,6 +157,11 @@ Note: We're only listing outstanding class updates.
|
||||
|
||||
* Ractor
|
||||
|
||||
* `Ractor.new` now isolates its block the way `Ractor.shareable_proc`
|
||||
does: the block may read outer local variables that are never reassigned
|
||||
and refer to shareable objects, instead of raising
|
||||
`Ractor::IsolationError` for any outer variable. [[Feature #22444]]
|
||||
|
||||
* `Ractor::Port#receive`, `Ractor.receive` and `Ractor.select` accept a
|
||||
`timeout:` keyword argument (in seconds) and return `nil` once it passes.
|
||||
`timeout: 0` never blocks: it takes a message if one is already queued
|
||||
@ -753,6 +758,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
|
||||
[Feature #22309]: https://bugs.ruby-lang.org/issues/22309
|
||||
[Feature #22400]: https://bugs.ruby-lang.org/issues/22400
|
||||
[Feature #22441]: https://bugs.ruby-lang.org/issues/22441
|
||||
[Feature #22444]: https://bugs.ruby-lang.org/issues/22444
|
||||
[PR #17201]: https://github.com/ruby/ruby/pull/17201
|
||||
[GH-psych #805]: https://github.com/ruby/psych/pull/805
|
||||
[prism-v1.8.0]: https://github.com/ruby/prism/releases/tag/v1.8.0
|
||||
|
||||
@ -100,7 +100,7 @@ File.write(<%= result.dump %>, __bmdv_ractors.shift)
|
||||
private
|
||||
|
||||
# id is to prevent:
|
||||
# can not isolate a Proc because it accesses outer variables (__bmdv_i)
|
||||
# cannot make a shareable Proc because the outer variable '__bmdv_i' may be reassigned.
|
||||
def while_loop(content, times, id:)
|
||||
if !times.is_a?(Integer) || times <= 0
|
||||
raise ArgumentError.new("Unexpected times: #{times.inspect}")
|
||||
|
||||
@ -826,18 +826,73 @@ assert_equal 'true', %q{
|
||||
end
|
||||
}
|
||||
|
||||
# given block Proc will be isolated, so can not access outer variables.
|
||||
# given block Proc is copied like Ractor.shareable_proc: it can read outer variables
|
||||
# which are shareable and never reassigned
|
||||
assert_equal '[42, "ok"]', %q{
|
||||
a = 42
|
||||
b = "ok".freeze
|
||||
r = Ractor.new do
|
||||
[a, b]
|
||||
end
|
||||
r.value
|
||||
}
|
||||
|
||||
# ... through nested blocks and with arguments
|
||||
assert_equal '[4, 8]', %q{
|
||||
a = 1
|
||||
r = Ractor.new(2) do |x|
|
||||
v = [1].map { |y| a + x + y }.first
|
||||
[v, v * 2]
|
||||
end
|
||||
r.value
|
||||
}
|
||||
|
||||
# an outer variable which is reassigned can not be accessed
|
||||
assert_equal 'Ractor::IsolationError', %q{
|
||||
begin
|
||||
a = true
|
||||
r = Ractor.new do
|
||||
a
|
||||
end
|
||||
a = false
|
||||
rescue => e
|
||||
e.class
|
||||
end
|
||||
}
|
||||
|
||||
# an outer variable holding an unshareable object can not be accessed
|
||||
assert_equal 'Ractor::IsolationError', %q{
|
||||
begin
|
||||
a = []
|
||||
r = Ractor.new do
|
||||
a
|
||||
end
|
||||
rescue => e
|
||||
e.class
|
||||
end
|
||||
}
|
||||
|
||||
# an outer variable can not be written
|
||||
assert_equal 'Ractor::IsolationError', %q{
|
||||
begin
|
||||
a = 1
|
||||
Ractor.new do
|
||||
a = 2
|
||||
end
|
||||
rescue => e
|
||||
e.class
|
||||
end
|
||||
}
|
||||
|
||||
# Ractor.new does not mutate the given Proc
|
||||
assert_equal '[false, false, 1, 2]', %q{
|
||||
pr = Proc.new { 1 }
|
||||
r1 = Ractor.new(&pr)
|
||||
r2 = Ractor.new(&pr)
|
||||
pr.instance_variable_set(:@foo, Object.new)
|
||||
[Ractor.shareable?(pr), pr.frozen?, r1.value, r2.value + 1]
|
||||
}
|
||||
|
||||
# eval with outer locals in a Ractor raises SyntaxError
|
||||
# [Bug #21522]
|
||||
assert_equal 'SyntaxError', %q{
|
||||
@ -1033,29 +1088,21 @@ assert_equal "can't modify instance variables of a shareable Ractor", %q{
|
||||
end
|
||||
}
|
||||
|
||||
# a Proc which already has ivars can not be isolated
|
||||
assert_equal 'can not isolate a Proc because it has instance variables', %q{
|
||||
pr = Proc.new{}
|
||||
# a Proc which already has ivars is fine: the ractor gets a copy without them
|
||||
assert_equal 'ok', %q{
|
||||
pr = Proc.new{ :ok }
|
||||
pr.instance_variable_set(:@iv, Object.new)
|
||||
|
||||
begin
|
||||
Ractor.new(&pr)
|
||||
rescue Ractor::IsolationError => e
|
||||
e.message
|
||||
end
|
||||
Ractor.new(&pr).value.to_s
|
||||
}
|
||||
|
||||
# freezing the Proc first does not skip the check
|
||||
assert_equal 'can not isolate a Proc because it has instance variables', %q{
|
||||
pr = Proc.new{}
|
||||
# ... also when it is frozen
|
||||
assert_equal 'ok', %q{
|
||||
pr = Proc.new{ :ok }
|
||||
pr.instance_variable_set(:@iv, Object.new)
|
||||
pr.freeze
|
||||
|
||||
begin
|
||||
Ractor.new(&pr)
|
||||
rescue Ractor::IsolationError => e
|
||||
e.message
|
||||
end
|
||||
Ractor.new(&pr).value.to_s
|
||||
}
|
||||
|
||||
# an ivar set by Proc#refined is internal, so it does not block isolation
|
||||
@ -1086,23 +1133,19 @@ assert_equal 'ok', %q{
|
||||
end.value.to_s
|
||||
}
|
||||
|
||||
# a Proc made shareable by Ractor.new can not be given ivars afterwards
|
||||
assert_equal "can't modify instance variables of a shareable Proc", %q{
|
||||
# Ractor.new does not make the given Proc shareable, so it can still be given ivars
|
||||
assert_equal "false", %q{
|
||||
HAX = -> { }
|
||||
Ractor.new(&HAX).join
|
||||
|
||||
begin
|
||||
HAX.instance_variable_set(:@foo, Object.new)
|
||||
rescue Ractor::IsolationError => e
|
||||
e.message
|
||||
end
|
||||
HAX.instance_variable_set(:@foo, Object.new)
|
||||
Ractor.shareable?(HAX).to_s
|
||||
}
|
||||
|
||||
# freezing a shareable object from another Ractor can not expose an ivar, since
|
||||
# none could be set after it became shareable
|
||||
assert_equal 'nil', %q{
|
||||
HAX = -> { }
|
||||
Ractor.new(&HAX).join
|
||||
HAX = Ractor.shareable_lambda { }
|
||||
|
||||
r = Ractor.new do
|
||||
HAX.freeze
|
||||
@ -3198,35 +3241,6 @@ assert_equal 'ok', %q{
|
||||
r.value
|
||||
}
|
||||
|
||||
# A Ractor creation that fails (IsolationError) after the child objspace exists must clean up
|
||||
# the creator's cover for it; otherwise a later global GC enumerates the dead child's objspace
|
||||
# twice and reads the freed shell.
|
||||
assert_equal 'ok', %q{
|
||||
x = 42 # capturing an outer local makes Ractor.new raise IsolationError
|
||||
worker = Ractor.new { loop { break if Ractor.receive == :quit } }
|
||||
begin
|
||||
Ractor.new { x }
|
||||
raise "isolation error did not fire"
|
||||
rescue Ractor::IsolationError
|
||||
end
|
||||
10.times { GC.start; 500.times { Object.new } }
|
||||
worker.send(:quit)
|
||||
worker.value
|
||||
100.times do |i|
|
||||
begin
|
||||
Ractor.new { x }
|
||||
raise "isolation error did not fire"
|
||||
rescue Ractor::IsolationError
|
||||
end
|
||||
if (i % 20).zero?
|
||||
Ractor.new { :ok }.value
|
||||
GC.start
|
||||
end
|
||||
end
|
||||
GC.start
|
||||
:ok
|
||||
}
|
||||
|
||||
# Moving a CoW shared-root string (a frozen root with an unshareable ivar) must not steal the
|
||||
# root's buffer; that would leave the remaining sharers reading freed memory.
|
||||
assert_equal 'ok', %q{
|
||||
|
||||
@ -89,15 +89,15 @@ r.name #=> 'my-first-ractor'
|
||||
### Block isolation
|
||||
|
||||
The Ractor executes `expr` in the given block.
|
||||
The given block will be isolated from its outer scope. To prevent sharing objects between ractors, outer variables, `self` and other information is isolated from the block.
|
||||
The given block will be isolated from its outer scope. To prevent sharing unshareable objects between ractors, `self` and other information is isolated from the block, and the block may only read outer variables which are never reassigned and refer to shareable objects.
|
||||
|
||||
This isolation occurs at Ractor creation time (when `Ractor.new` is called). If the given block is not able to be isolated because of outer variables or `self`, an error will be raised.
|
||||
|
||||
```ruby
|
||||
begin
|
||||
a = true
|
||||
a = []
|
||||
r = Ractor.new do
|
||||
a #=> Ractor::IsolationError because this block accesses outer variable `a`.
|
||||
a #=> Ractor::IsolationError because this block accesses outer variable `a`, which refers to an unshareable object.
|
||||
end
|
||||
r.join # wait for ractor to finish
|
||||
rescue Ractor::IsolationError
|
||||
|
||||
19
ractor.c
19
ractor.c
@ -915,9 +915,28 @@ rb_ractor_main_setup(rb_vm_t *vm, rb_ractor_t *r, rb_thread_t *th)
|
||||
RB_GC_GUARD(rv);
|
||||
}
|
||||
|
||||
// Same semantics as Ractor.shareable_proc: the block is duplicated (the caller's
|
||||
// Proc is never mutated), may read outer variables which are never reassigned and
|
||||
// whose values are shareable, and `self` is replaced by the new Ractor when run.
|
||||
static VALUE
|
||||
ractor_isolated_block(VALUE block)
|
||||
{
|
||||
if (vm_proc_iseq(block)) {
|
||||
return rb_proc_ractor_make_shareable(rb_proc_dup(block), Qnil);
|
||||
}
|
||||
else {
|
||||
// Symbol procs, method procs, etc.: they have no outer variables.
|
||||
return rb_proc_isolate(block);
|
||||
}
|
||||
}
|
||||
|
||||
static VALUE
|
||||
ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VALUE args, VALUE block)
|
||||
{
|
||||
/* Make the shareable copy before anything is allocated: it may raise
|
||||
* Ractor::IsolationError, and the original block is left untouched. */
|
||||
block = ractor_isolated_block(block);
|
||||
|
||||
VALUE rv = ractor_alloc(self);
|
||||
rb_ractor_t *r = RACTOR_PTR(rv);
|
||||
ractor_init(r, name, loc);
|
||||
|
||||
13
ractor.rb
13
ractor.rb
@ -9,16 +9,16 @@
|
||||
# concerns such as data-races and race-conditions are not possible. The other benefit is parallelism.
|
||||
#
|
||||
# To achieve this, object sharing is limited across ractors.
|
||||
# Unlike in threads, ractors can't access all the objects available in other ractors. For example, objects normally
|
||||
# available through variables in the outer scope are prohibited from being used across ractors.
|
||||
# Unlike in threads, ractors can't access all the objects available in other ractors. For example, unshareable
|
||||
# objects available through variables in the outer scope are prohibited from being used across ractors.
|
||||
#
|
||||
# a = 1
|
||||
# a = []
|
||||
# r = Ractor.new {puts "I am in Ractor! a=#{a}"}
|
||||
# # fails immediately with
|
||||
# # Ractor::IsolationError (can not isolate a Proc because it accesses outer variables (a).)
|
||||
# # Ractor::IsolationError (cannot make a shareable Proc because it can refer unshareable object [] from variable 'a')
|
||||
#
|
||||
# The object must be explicitly shared:
|
||||
# a = 1
|
||||
# a = []
|
||||
# r = Ractor.new(a) { |a1| puts "I am in Ractor! a=#{a1}"}
|
||||
#
|
||||
# On CRuby (the default implementation), the Global Virtual Machine Lock (GVL) is held per ractor, so
|
||||
@ -199,7 +199,8 @@ class Ractor
|
||||
#
|
||||
# Creates a new \Ractor with args and a block.
|
||||
#
|
||||
# The given block (Proc) is isolated (can't access any outer variables). +self+
|
||||
# The given block (Proc) is isolated (can only read outer variables which are
|
||||
# never reassigned and refer to shareable objects). +self+
|
||||
# inside the block will refer to the current \Ractor.
|
||||
#
|
||||
# r = Ractor.new { puts "Hi, I am #{self.inspect}" }
|
||||
|
||||
@ -813,22 +813,56 @@ class TestRactor < Test::Unit::TestCase
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_new_raises_isolation_error_if_outer_variables_are_accessed
|
||||
assert_raise(Ractor::IsolationError) do
|
||||
def test_ractor_new_allows_reading_shareable_outer_variables
|
||||
assert_ractor(<<~'RUBY')
|
||||
channel = Ractor::Port.new
|
||||
Ractor.new(channel) do
|
||||
r = Ractor.new(channel) do
|
||||
inbound_work = Ractor::Port.new
|
||||
channel << inbound_work
|
||||
end
|
||||
end
|
||||
assert_kind_of Ractor::Port, channel.receive
|
||||
r.join
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_new_raises_isolation_error_if_unshareable_outer_variables_are_accessed
|
||||
assert_ractor(<<~'RUBY')
|
||||
assert_raise_with_message(Ractor::IsolationError, /unshareable object \[\]/) do
|
||||
list = []
|
||||
Ractor.new { list }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_new_raises_isolation_error_if_outer_variables_are_reassigned
|
||||
assert_ractor(<<~'RUBY')
|
||||
assert_raise_with_message(Ractor::IsolationError, /'n' may be reassigned/) do
|
||||
n = 1
|
||||
Ractor.new { n }
|
||||
n = 2
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_new_does_not_modify_the_block
|
||||
assert_ractor(<<~'RUBY')
|
||||
pr = proc { 42 }
|
||||
assert_equal 42, Ractor.new(&pr).value
|
||||
assert_not_predicate pr, :frozen?
|
||||
assert_not_operator Ractor, :shareable?, pr
|
||||
assert_equal 42, Ractor.new(&pr).value
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_new_raises_isolation_error_if_proc_uses_yield
|
||||
assert_raise(Ractor::IsolationError) do
|
||||
Ractor.new do
|
||||
yield
|
||||
assert_ractor(<<~'RUBY')
|
||||
def ractor_with_yield
|
||||
Ractor.new do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
assert_raise(Ractor::IsolationError) { ractor_with_yield }
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_does_not_inherit_fiber_storage
|
||||
@ -919,19 +953,20 @@ class TestRactor < Test::Unit::TestCase
|
||||
RUBY
|
||||
end
|
||||
|
||||
# A Ractor.new that fails with IsolationError (stillborn) must still clean up the
|
||||
# half-created objspace (regression guard for double enumeration / use-after-free).
|
||||
# A Ractor.new that fails with an uncopyable argument (stillborn) must still clean
|
||||
# up the half-created objspace (regression guard for double enumeration /
|
||||
# use-after-free).
|
||||
def test_stillborn_ractor_gc
|
||||
assert_ractor(<<~'RUBY', timeout: 60)
|
||||
x = 42 # capturing an outer local makes Ractor.new raise IsolationError
|
||||
x = Thread.current # an uncopyable argument makes Ractor.new raise Ractor::Error
|
||||
worker = Ractor.new { loop { break if Ractor.receive == :quit } }
|
||||
assert_raise(Ractor::IsolationError) { Ractor.new { x } }
|
||||
assert_raise(Ractor::Error) { Ractor.new(x) { } }
|
||||
10.times { GC.start; 500.times { Object.new } }
|
||||
GC.verify_internal_consistency
|
||||
worker.send(:quit)
|
||||
worker.value
|
||||
100.times do |i|
|
||||
assert_raise(Ractor::IsolationError) { Ractor.new { x } }
|
||||
assert_raise(Ractor::Error) { Ractor.new(x) { } }
|
||||
if (i % 20).zero?
|
||||
Ractor.new { :ok }.value
|
||||
GC.start
|
||||
@ -947,7 +982,7 @@ class TestRactor < Test::Unit::TestCase
|
||||
# built by another thread of the same Ractor: a stillborn child frees that objspace.
|
||||
def test_stillborn_ractor_with_free_off_gvl
|
||||
assert_ractor(<<~'RUBY', require: '-test-/gvl/call_without_gvl', timeout: 60)
|
||||
x = 42 # capturing an outer local makes Ractor.new raise IsolationError
|
||||
x = Thread.current # an uncopyable argument makes Ractor.new raise Ractor::Error
|
||||
stop = false
|
||||
ready = Queue.new
|
||||
freers = 4.times.map do
|
||||
@ -959,8 +994,8 @@ class TestRactor < Test::Unit::TestCase
|
||||
freers.size.times { ready.pop } # all of them are churning before we start
|
||||
2_000.times do
|
||||
begin
|
||||
Ractor.new { x }
|
||||
rescue Ractor::IsolationError
|
||||
Ractor.new(x) { }
|
||||
rescue Ractor::Error
|
||||
end
|
||||
end
|
||||
stop = true
|
||||
|
||||
4
thread.c
4
thread.c
@ -942,7 +942,7 @@ thread_create_core(VALUE thval, struct thread_create_params *params)
|
||||
th->ec->ractor_id = rb_ractor_id(th->ractor);
|
||||
th->ec->serial = th->ractor->next_ec_serial++;
|
||||
th->ractor->threads.main = th;
|
||||
th->invoke_arg.proc.proc = rb_proc_isolate_bang(params->proc, Qnil);
|
||||
th->invoke_arg.proc.proc = params->proc; // already a shareable copy (see ractor_create)
|
||||
th->invoke_arg.proc.args = INT2FIX(RARRAY_LENINT(params->args));
|
||||
th->invoke_arg.proc.kw_splat = rb_keyword_given_p();
|
||||
break;
|
||||
@ -1197,7 +1197,7 @@ rb_thread_create_ractor(rb_ractor_t *r, VALUE args, VALUE proc)
|
||||
|
||||
VALUE thval = create_ractor_alloc_thread(r, cr, ec);
|
||||
|
||||
/* Creation can still fail before vm_insert_ractor (an IsolationError, say), and a
|
||||
/* Creation can still fail before vm_insert_ractor (NoMemoryError, say), and a
|
||||
* left-over cover would enumerate the dead child's objspace twice and dangle after
|
||||
* the merge: on failure hand the objspace to zombie_objspaces under the VM lock,
|
||||
* drop the cover, NULL r->objspace. */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user