Project

General

Profile

Feature #19472

Updated by ko1 (Koichi Sasada) almost 2 years ago

This ticket propose `Ractor::Selector` API to wait multiple ractor events. 

 Now, if we want to wait for taking from r1, r2 and r3, we can use `Ractor.select()` like that. 


 ```ruby 
 r, v = Ractor.select(r1, r2, r3) 
 p "taking an object #{v} from #{r}" 
 ``` 

 With proposed `Ractor::Selector` API, we can write the following: 

 ```ruby 
 selector = Ractor.selector.new(r1, r2) # make a waiting set with r1 and r2 
 selector.add(r3) # we can add r3 to the waiting set after that. 
 selector.add(r4) 
 selector.remove(r4) # we can remove r4 from the waiting set. 

 r, v = selector.wait 
 p "taking an object #{v} from #{r}" 
 ``` 

 * `Ractor::Selector.new(*ractors)`: creates create a selector. selector 
 * `Ractor::Selector#add(r)`: adds add `r` to the waiting set. list 
 * `Ractor::Selector#remove(r)`: removes remove `r` from the waiting set. list 
 * `Ractor::Selector#clear`: remove all ractors from the waiting set. list 
 * `Ractor::Selector#empty?`: returns if the waiting set is empty or not. 
 * `Ractor::Selector#wait`: waits wait for the ractor events from the waiting set. 

 https://github.com/ruby/ruby/blob/master/ractor.rb#L380 https://github.com/ruby/ruby/pull/7371/files#diff-2be07f7941fed81f90e2947cdd9a91a5775d0c94335e8332b4805d264380b255R380 

 The advantages comparing with `Ractor.select` are: 

 * (1) (API design) We can preset the waiting set before waiting. Providing unified way to manage a waiting set list seems better. 
 * (2) (Performance) It is lighter than passing an array object to the `Ractor.select(*rs)` if `rs` is bigger and bigger. 

 For (2), it is important to supervise thousands of ractors. 

 `Ractor::Selector#wait` also has additional features: 

 * `wait(receive: true)` also waits receiving. 
   * `Ractor.select(*rs, Ractor.current)` does same, but I believe `receive: true` keyword is more direct to understand. 
 * `wait(yield_value: obj, move: true/false)` also waits yielding. 
   * Same as `Ractor.select(yield_value: obj, move: true/false)` 
 * If a ractor `r` is closing, then `#wait` removes `r` automatically. 
 * If there is no waiting ractors, it raises an exception (now `Ractor::Error` is raised but it should be a better exception class) 

 With automatic removing, we can write the code to wait n tasks. 

 ```ruby 
 rs = n.times.map{ Ractor.new{ do_task } } 
 selector = Ractor::Selector.new(*rs) 

 loop do 
   r, v = selector.wait 
   handle_answers(r, v) 
 rescue Ractor::Error 
   p :all_tasks_done 
 end 
 ``` 

 Without auto removing, we can write the following code. 

 ```ruby 
 rs = n.times.map{ Ractor.new{ do_task } } 
 selector = Ractor::Selector.new(*rs) 

 loop do 
   r, v = selector.wait 
   handle_answers(r, v) 
 rescue Ractor::ClosedError => e 
   selector.remove e.ractor 
 rescue Ractor::Error 
   p :all_tasks_done 
 end 

 # or on this case worker ractors only yield one value (at exit) so the following code works as well. 

 loop do 
   r, v = selector.wait 
   handle_answers(r, v) 
   selector.remove r 
 rescue Ractor::Error 
   p :all_tasks_done 
 end 

 ```  

 I already merged it but I want to discuss about the spec. 

 Discussion: 

 * The name `Selector` is acceptable? 
 * Auto-removing seems convenient but it can hide the behavior. 
   * allow auto-removing 
   * allow auto-removing as configurable option 
     * per ractor or per selector 
     * which is default? 
   * disallow auto-removing 
 * What happens on no taking ractors 
   * raise an exception (which exception?) 
   * return nil simply 

 maybe and more... 

Back