ruby-changes:62820
From: Koichi <ko1@a...>
Date: Thu, 3 Sep 2020 21:11:27 +0900 (JST)
Subject: [ruby-changes:62820] 79df14c04b (master): Introduce Ractor mechanism for parallel execution
https://git.ruby-lang.org/ruby.git/commit/?id=79df14c04b From 79df14c04b452411b9d17e26a398e491bca1a811 Mon Sep 17 00:00:00 2001 From: Koichi Sasada <ko1@a...> Date: Tue, 10 Mar 2020 02:22:11 +0900 Subject: Introduce Ractor mechanism for parallel execution This commit introduces Ractor mechanism to run Ruby program in parallel. See doc/ractor.md for more details about Ractor. See ticket [Feature #17100] to see the implementation details and discussions. [Feature #17100] This commit does not complete the implementation. You can find many bugs on using Ractor. Also the specification will be changed so that this feature is experimental. You will see a warning when you make the first Ractor with `Ractor.new`. I hope this feature can help programmers from thread-safety issues. diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb new file mode 100644 index 0000000..026b6ad --- /dev/null +++ b/bootstraptest/test_ractor.rb @@ -0,0 +1,516 @@ https://github.com/ruby/ruby/blob/trunk/bootstraptest/test_ractor.rb#L1 +# Ractor.current returns a current ractor +assert_equal 'Ractor', %q{ + Ractor.current.class +} + +# Ractor.new returns new Ractor +assert_equal 'Ractor', %q{ + Ractor.new{}.class +} + +# Ractor.new must call with a block +assert_equal "must be called with a block", %q{ + begin + Ractor.new + rescue ArgumentError => e + e.message + end +} + + +# A return value of a Ractor block will be a message from the Ractor. +assert_equal 'ok', %q{ + # join + r = Ractor.new do + 'ok' + end + r.take +} + +# Passed arguments to Ractor.new will be a block parameter +# The values are passed with Ractor-communication pass. +assert_equal 'ok', %q{ + # ping-pong with arg + r = Ractor.new 'ok' do |msg| + msg + end + r.take +} + +assert_equal 'ok', %q{ + # ping-pong with two args + r = Ractor.new 'ping', 'pong' do |msg, msg2| + [msg, msg2] + end + 'ok' if r.take == ['ping', 'pong'] +} + +# Ractor#send passes an object with copy to a Ractor +# and Ractor.recv in the Ractor block can receive the passed value. +assert_equal 'ok', %q{ + r = Ractor.new do + msg = Ractor.recv + end + r.send 'ok' + r.take +} + +# Ractor.select(*ractors) receives a values from a ractors. +# It is similar to select(2) and Go's select syntax. +# The return value is [ch, received_value] +assert_equal 'ok', %q{ + # select 1 + r1 = Ractor.new{'r1'} + r, obj = Ractor.select(r1) + 'ok' if r == r1 and obj == 'r1' +} + +assert_equal '["r1", "r2"]', %q{ + # select 2 + r1 = Ractor.new{'r1'} + r2 = Ractor.new{'r2'} + rs = [r1, r2] + as = [] + r, obj = Ractor.select(*rs) + rs.delete(r) + as << obj + r, obj = Ractor.select(*rs) + as << obj + as.sort #=> ["r1", "r2"] +} + +assert_equal 'true', %q{ + def test n + rs = (1..n).map do |i| + Ractor.new(i) do |i| + "r#{i}" + end + end + as = [] + all_rs = rs.dup + + n.times{ + r, obj = Ractor.select(*rs) + as << [r, obj] + rs.delete(r) + } + + if as.map{|r, o| r.inspect}.sort == all_rs.map{|r| r.inspect}.sort && + as.map{|r, o| o}.sort == (1..n).map{|i| "r#{i}"}.sort + 'ok' + else + 'ng' + end + end + + 30.times.map{|i| + test i + }.all?('ok') +} + +# Outgoing port of a ractor will be closed when the Ractor is terminated. +assert_equal 'ok', %q{ + r = Ractor.new do + 'finish' + end + + r.take + sleep 0.1 # wait for terminate + + begin + o = r.take + rescue Ractor::ClosedError + 'ok' + else + "ng: #{o}" + end +} + +assert_equal 'ok', %q{ + r = Ractor.new do + end + + r.take # closed + sleep 0.1 # wait for terminate + + begin + r.send(1) + rescue Ractor::ClosedError + 'ok' + else + 'ng' + end +} + +# multiple Ractors can recv (wait) from one Ractor +assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ + pipe = Ractor.new do + loop do + Ractor.yield Ractor.recv + end + end + + RN = 10 + rs = RN.times.map{|i| + Ractor.new pipe, i do |pipe, i| + msg = pipe.take + msg # ping-pong + end + } + RN.times{|i| + pipe << i + } + RN.times.map{ + r, n = Ractor.select(*rs) + rs.delete r + n + }.sort +} + +# Ractor.select also support multiple take, recv and yiled +assert_equal '[true, true, true]', %q{ + RN = 10 + CR = Ractor.current + + rs = (1..RN).map{ + Ractor.new do + CR.send 'send' + CR.take #=> 'sendyield' + 'take' + end + } + recv = [] + take = [] + yiel = [] + until rs.empty? + r, v = Ractor.select(CR, *rs, yield_value: 'yield') + case r + when :recv + recv << v + when :yield + yiel << v + else + take << v + rs.delete r + end + end + [recv.all?('sendyield'), yiel.all?(nil), take.all?('take')] +} + +# multiple Ractors can send to one Ractor +assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ + pipe = Ractor.new do + loop do + Ractor.yield Ractor.recv + end + end + + RN = 10 + RN.times.map{|i| + Ractor.new pipe, i do |pipe, i| + pipe << i + end + } + RN.times.map{ + pipe.take + }.sort +} + +# an exception in a Ractor will be re-raised at Ractor#recv +assert_equal '[RuntimeError, "ok", true]', %q{ + r = Ractor.new do + raise 'ok' # exception will be transferred receiver + end + begin + r.take + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true + end +} + +# unshareable object are copied +assert_equal 'false', %q{ + obj = 'str'.dup + r = Ractor.new obj do |msg| + msg.object_id + end + + obj.object_id == r.take +} + +# To copy the object, now Marshal#dump is used +assert_equal 'no _dump_data is defined for class Thread', %q{ + obj = Thread.new{} + begin + r = Ractor.new obj do |msg| + msg + end + rescue TypeError => e + e.message #=> no _dump_data is defined for class Thread + else + 'ng' + end +} + +# send sharable and unsharable objects +assert_equal "[[[1, true], [:sym, true], [:xyzzy, true], [\"frozen\", true], " \ + "[(3/1), true], [(3+4i), true], [/regexp/, true], [C, true]], " \ + "[[\"mutable str\", false], [[:array], false], [{:hash=>true}, false]]]", %q{ + r = Ractor.new do + while v = Ractor.recv + Ractor.yield v + end + end + + class C + end + + sharable_objects = [1, :sym, 'xyzzy'.to_sym, 'frozen'.freeze, 1+2r, 3+4i, /regexp/, C] + + sr = sharable_objects.map{|o| + r << o + o2 = r.take + [o, o.object_id == o2.object_id] + } + + ur = unsharable_objects = ['mutable str'.dup, [:array], {hash: true}].map{|o| + r << o + o2 = r.take + [o, o.object_id == o2.object_id] + } + [sr, ur].inspect +} + +# move example2: String +# touching moved object causes an error +assert_equal 'hello world', %q{ + # move + r = Ractor.new do + obj = Ractor.recv + obj << ' world' + end + + str = 'hello' + r.send str, move: true + modified = r.take + + begin + str << ' exception' # raise Ractor::MovedError + rescue Ractor::MovedError + modified #=> 'hello world' + else + raise 'unreachable' + end +} + +# move example2: Array +assert_equal '[0, 1]', %q{ + r = Ractor.new do + ary = Ractor.recv + ary << 1 + end + + a1 = [0] + r.send a1, move: true + a2 = r.take + begin + a1 << 2 # raise Ractor::MovedError + rescue Ractor::MovedError + a2.inspect + end +} + +# move with yield +assert_equal 'hello', %q{ + r = Ractor.new do + Thread.current.report_on_exception = false + obj = 'hello' + Ractor.yield obj, move: true + obj << 'world' + end + + str = r.take + begin + r.take + rescue Ractor::RemoteError + str #=> "hello" + end +} + +# Access to global-variables are prohibitted +assert_equal 'can not access global variables $gv from non-main Ractors', %q{ + $gv = 1 + r = Ractor.new do + $gv + end + + begin + r.take + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Access to global-variables are prohibitted +assert_equal 'can not access global variables $gv from non-main Ractors', %q{ + r = Ractor.new do + $gv = 1 + end + + begin + r.take + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# $stdin,out,err is Ractor local, but shared fds +assert_equal 'ok', %q{ + r = Ractor.new do + [$stdin, $stdout, $stderr].map{|io| + [io.object_id, io.fileno] + } + end + + [$stdin, $stdout, $stderr].zip(r.take){|io, (oid, fno)| + raise "should not be different object" if io.object_id == oid + raise "fd should be same" unless io.fileno == fno + } + 'ok' +} + +# selfs are different objects +assert_equal 'false', %q{ + r = Ractor.new do + self.object_id + end + r.take == self.object_id #=> false +} + +# self is a Ractor instance +assert_equal 'true', %q{ + r = Ractor.new do + self.object_id + end + r.object_id == r.take #=> true +} + +# given block Proc will be isolated, so can not access outer variables. +assert_equal 'ArgumentError', %q{ + begin + a = true + r = Ractor.new do + a + end + rescue => e + e.class + end +} + +# ivar in sharable-objects are not allowed to access from non-main Ractor +assert_equal 'can not access instance variables of classes/modules from non-main Ractors', %q{ + class C + @iv = 'str' + end + + r = Ractor.new do + class C + p @iv + end + end + + + begin + r.take + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# ivar in sharable-objects are not allowed to access from non-main Ractor +assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{ + shared = Ractor.new{} + shared.instance_variable_set(:@iv, 'str') + + r = Ractor.new shared do |shared| + p shared.instance_variable_get(:@iv) + end + + begin + r.take + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# cvar in sharable-objects are not allowed to access from non-main Ractor +assert_equal 'can not access class variables from non-main Ractors', % (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/