From: "Eregon (Benoit Daloze) via ruby-core" Date: 2025-01-14T19:22:04+00:00 Subject: [ruby-core:120668] [Ruby master Feature#21033] Allow lambdas that don't access `self` to be Ractor shareable Issue #21033 has been updated by Eregon (Benoit Daloze). tenderlovemaking (Aaron Patterson) wrote in #note-11: > Adding syntax makes it much harder to port existing applications to use Ractors. > I'm specifically looking at adding Ractor support to Rails at the moment, and there are blocks that are shared between threads and would need to be shared between Ractors. I think porting any non-trivial code to use Ractors is a huge effort (I would think most gems out there do not work when used inside a Ractor, which seems a huge compatibility problem). Ractor seems fundamentally incompatible with much of how Ruby code works, a lot of it being mutation, sharing state, configuration, etc globally and having access to everything everywhere. It's like forcing the actor model when there are so many other concurrency models and the actor one is only good for some (small) subset of it. So my impression is the issue of procs/lambdas being not shareable is just one of the many hurdles (but maybe I'm wrong). > New syntax / idioms would mean we have to change all lambda creation sites. I understand why new syntax would be very annoying. But one could define `def isolated(&) = nil.instance_exec(&)` or so and then use `isolated { lambda { ... } }`. Or maybe a new argument/kwarg to `#lambda` then or some new method (not sure how well that would work on older versions though). Or a new magic comment? I think it makes some sense to change all blocks meant to be passed to a Ractor, because changing `self` to `nil` not where the block is declared would lead to lots of confusion (NoMethodError on nil much later, looking at the code `self` can't possibly be `nil`), it breaks the intent of the person who wrote that block, and also to anyone reading that code. It seems nice if we could reliably auto-detect blocks which don't rely on `self`, indeed, but I think that's basically impossible with things like `debug_inspector` and `Proc#binding` and `eval`. I think it apply to a pretty small set of blocks, as I would guess most blocks rely on `self`, e.g. to call an instance method (or even just to call a Kernel instance method). Also it doesn't seem nice if a program would behave differently by adding a `p(value)` in some block to debug (which adds a `putself`) and that would cause a Ractor::IsolationError or so. ---------------------------------------- Feature #21033: Allow lambdas that don't access `self` to be Ractor shareable https://bugs.ruby-lang.org/issues/21033#change-111493 * Author: tenderlovemaking (Aaron Patterson) * Status: Open ---------------------------------------- Hi, I would like to allow lambdas that don't access `self` to be eligible for Ractor shareability regardless of the shareability status of `self`. Consider the following code: ```ruby class Foo def make_lambda x = 123 lambda { x } end end Ractor.make_shareable(Foo.new.make_lambda) ``` With Ruby 3.4.X, this will raise an exception. The reason is because `self`, which is an unfrozen instance of `Foo`, is not shareable. However, we can see from the code that the lambda doesn't access `self`. I would like to make lambdas such as the ones above eligible for shareability, and I've submitted a patch [here](https://github.com/ruby/ruby/pull/12567). I think we can detect access to `self` by scanning the instructions in the lambda. Any references to `putself`, `getinstancevariable`, or `setinstancevariable` will result in using the default behavior (checking the frozen status of `self`). ## Considerations ### What about `eval`? I think that `eval` is not a problem because calling eval has an implicit reference to `self`: ``` $ ./miniruby --dump=insns -e 'lambda { eval("123") }' == disasm: #@-e:1 (1,0)-(1,22)> 0000 putself ( 1)[Li] 0001 send , block in
0004 leave == disasm: #@-e:1 (1,7)-(1,22)> 0000 putself ( 1)[LiBc] 0001 putchilledstring "123" 0003 opt_send_without_block 0005 leave [Br] ``` If we try to call `eval` inside the lambda, there will be an implicit `putself` instruction added which means we will fall back to the old behavior. ### What about `binding`? If you call `binding` from inside the `lambda` there will be a `putself` instruction so we fall back to the old behavior. This is the same as the `eval` case. ### What about `binding` via a local? If you assign `binding` to a local, shareability will fail because the `lambda` references an unshareable local: ```ruby class Foo def make_lambda x = binding lambda { x } end end b = Foo.new.make_lambda # exception because local `x` is not shareable Ractor.make_shareable(b) ``` ### What about accessing `binding` via the proc itself? The lambda can references itself via a local and access binding, but again this will fail isolation when locals are scanned: ```ruby class Foo def make_lambda x = lambda { x.binding.eval("self") } end end b = Foo.new.make_lambda # exception because local `x` is not shareable Ractor.make_shareable(b) ``` I _think_ I've covered all cases where `self` can possibly escape. I would appreciate any feedback. Again, [here is the patch](https://github.com/ruby/ruby/pull/12567). Thanks. ---Files-------------------------------- 0001-Allow-lambdas-that-don-t-access-self-to-be-made-shar.patch (6.51 KB) -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/