[前][次][番号順一覧][スレッド一覧]

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/

[前][次][番号順一覧][スレッド一覧]