Project

General

Profile

Bug #21039

Updated by Eregon (Benoit Daloze) 1 day ago

```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

Back