Feature #17159
openextend `define_method` for Ractor
Description
Ractor prohibits use of non-isolated Proc
s.
Non-isolated example is here:
s = "foo"
pr = Proc.new{ p s }
This Proc pr
can not be shared among ractors because outer variable s
can contain an unshareable object. Also outer binding is a mutable object. Sharing it can lead race conditions.
Because of these reasons, define_method
is also a problem on a multi-Ractor program.
(current implementation allows it just because check is not implemented, and it leads BUG).
I think there are several patterns when define_method
is needed.
(1) To choose method names on-the-fly
name = ...
define_method(name){ nil }
(2) To embed variables to the code
10.times{|i|
define_method("foo#{i}"){ i }
}
(3) To use global state by local variables
cnt = 0
define_method("inc"){ cnt += 1 }
(4) Others I can't imagine
(1) is easy. We can allow define_method(name, &Proc{nil}.isolate)
.
(3) can never be OK. It introduces data races/race conditions. For this purpose one need to use shared hash.
STATE = SharedHash.new(cnt: 0)
define_method("inc"){ STATE.transaction{ STATE[:cnt] += 1 }}
I think there are many (2) patterns that should be saved.
To help (2) pattern, the easiest way is to use eval
.
10.times{|i|
eval("def foo#{i} #{i}; end")
}
However, eval
has several issues (it has huge freedom to explode the program, editor's syntax highlighting and so on).
Another approach is to embed the current value to the code, like this:
i = 0
define_method("foo", ractorise: true){ i }
#=> equivalent to:
# define_method("foo"){ 0 }
# so that if outer scope's i changed, not affected.
i = 1
foo #=> 0
s = ""
define_method("bar", ractorise: true){ s }
#=> equivalent to:
# define_method("bar"){ "" }
# so that if outer scope's s or s's value, it doesn't affect
s << "x"
bar #=> ""
However, it is very differenct from current Proc semantics.
Another idea is to specify embedding value like this:
i = 0
define_method("foo", i: i){ i }
#=> equivalent to:
# define_method("foo"){ 0 }
# so that if outer scope's i changed, not affected.
i = 1
foo #=> 0
s = ""
define_method("bar", s: s){ s }
#=> equivalent to:
# define_method("bar"){ "" }
# so that if outer scope's s or s's value, it doesn't affect
s << "x"
bar #=> ""
i: i
and s: s
are redundant. However, if there are no outer variable i
or s
, the i
and s
in blocks are compiled to send(:i)
or send(:s)
. But I agree these method invocation should be replaced is another idea.
Thoughts?
Thanks,
Koichi