
Issue #21039 has been updated by jhawthorn (John Hawthorn). tenderlovemaking (Aaron Patterson) wrote in #note-21:
```ruby foo = 123 Ractor.shareable_proc { foo } foo = Object.new # reassignment isn't allowed ```
All the other new rules are great and improve consistency, but I don't think this one is viable or necessary. To implement it would require examining a CFG (which we don't currently build in CRuby) and find every assignment which could come after. We'd also have to scan the rescue table (ex. `begin; p = proc { e }; raise; rescue => e; end`) and all child iseqs (ex. `1.times { foo = 456 }`). It makes very little sense to me that example 4 is fine but example 2 isn't. Since in a normal program those are the same thing. We'd also have to throw in the same caveat for `binding.local_variable_{set,get}`. So it all amounts to a lot of work that's more surprising to the user. ``` foo = 123 # Standard proc, using existing mutable env regular_proc = -> { foo } # New shareable proc object, the env is copied and immutable shared_proc = Ractor.shareable_proc(®ular_proc) foo = 456 # Should be allowed # For regular proc, standard, existing behaviour, we get the new value regular_proc.call # => 456 # For shareable proc, uses copied readonly env shared_proc.call # => 123 ``` I don't think this is a significant departure from existing semantics. It's reasonable for a different object, which has been explicitly created with these semantics, behaves differently than the original. It's also not dissimilar to using, say, instance_exec to change self (only here it's a different environment). This is also much more similar to the existing `Ractor.new(foo) {|foo| }`, which prohibits reading parent's locals, but doesn't forbid assignment after creating a Ractor (which would be MORE strict). Shareable proc just makes the copies implicit. ---------------------------------------- Feature #21039: Ractor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks https://bugs.ruby-lang.org/issues/21039#change-114291 * Author: Eregon (Benoit Daloze) * Status: Assigned * Assignee: ko1 (Koichi Sasada) ---------------------------------------- ```ruby def make_counter count = 0 nil.instance_exec do [-> { count }, -> { count += 1 }] end end get, increment = make_counter reader = Thread.new { sleep 0.01 loop do p get.call sleep 0.1 end } writer = Thread.new { loop do increment.call sleep 0.1 end } ractor_thread = Thread.new { sleep 1 Ractor.make_shareable(get) } sleep 2 ``` This prints: ``` 1 2 3 4 5 6 7 8 9 10 10 10 10 10 10 10 10 10 10 10 ``` But it should print 1..20, and indeed it does when commenting out the `Ractor.make_shareable(get)`. This shows a given block/Proc instance is concurrently broken by `Ractor.make_shareable`, IOW Ractor is breaking fundamental Ruby semantics of blocks and their captured/outer variables or "environment". It's expected that `Ractor.make_shareable` can `freeze` objects and that may cause some FrozenError, but here it's not a FrozenError, it's wrong/stale values being read. I think what should happen instead is that `Ractor.make_shareable` should create a new Proc and mutate that. However, if the Proc is inside some other object and not just directly the argument, that wouldn't work (like `Ractor.make_shareable([get])`). So I think one fix would to be to only accept Procs for `Ractor.make_shareable(obj, copy: true)`. FWIW that currently doesn't allow Procs, it gives `<internal:ractor>:828:in 'Ractor.make_shareable': allocator undefined for Proc (TypeError)`. It makes sense to use `copy` here since `make_shareable` effectively takes a copy/snapshot of the Proc's environment. I think the only other way, and I think it would be a far better way would be to not support making Procs shareable with `Ractor.make_shareable`. Instead it could be some new method like `isolated { ... }` or `Proc.isolated { ... }` or `Proc.snapshot_outer_variables { ... }` or so, only accepting a literal block (to avoid mutating/breaking an existing block), and that would snapshot outer variables (or require no outer variables like Ractor.new's block, or maybe even do `Ractor.make_shareable(copy: true)` on outer variables) and possibly also set `self` since that's anyway needed. That would make such blocks with different semantics explicit, which would fix the problem of breaking the intention of who wrote that block and whoever read that code, expecting normal Ruby block semantics, which includes seeing updated outer variables. Related: #21033 https://bugs.ruby-lang.org/issues/18243#note-5 Extracted from https://bugs.ruby-lang.org/issues/21033#note-14 -- https://bugs.ruby-lang.org/