Feature #19472
Updated by ko1 (Koichi Sasada) over 1 year 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...