ruby-changes:64350
From: Victor <ko1@a...>
Date: Sun, 20 Dec 2020 03:05:01 +0900 (JST)
Subject: [ruby-changes:64350] 1f565ac6d9 (master): Add documentation for Ractor (#3895)
https://git.ruby-lang.org/ruby.git/commit/?id=1f565ac6d9 From 1f565ac6d98e902f51a0ea7b8b4aa85693deea6e Mon Sep 17 00:00:00 2001 From: Victor Shepelev <zverok.offline@g...> Date: Sat, 19 Dec 2020 20:04:40 +0200 Subject: Add documentation for Ractor (#3895) diff --git a/.document b/.document index 0e0969a..8616203 100644 --- a/.document +++ b/.document @@ -20,6 +20,7 @@ kernel.rb https://github.com/ruby/ruby/blob/trunk/.document#L20 pack.rb trace_point.rb warning.rb +ractor.rb # the lib/ directory (which has its own .document file) lib diff --git a/ractor.c b/ractor.c index 08150f4..0565827 100644 --- a/ractor.c +++ b/ractor.c @@ -1933,6 +1933,100 @@ ractor_moved_missing(int argc, VALUE *argv, VALUE self) https://github.com/ruby/ruby/blob/trunk/ractor.c#L1933 rb_raise(rb_eRactorMovedError, "can not send any methods to a moved object"); } +/* + * Document-class: Ractor::ClosedError + * + * Raised when an attempt is made to take something from the Ractor's outgoing port, + * but it is closed with Ractor#close_outgoing, or to send something to Ractor's + * incoming port, and it is closed with Ractor#close_incoming, or an attempt to + * send/take something with ractor which was already terminated. + * + * r = Ractor.new { sleep(500) } + * r.close_outgoing + * r.take # Ractor::ClosedError + * + * ClosedError is a descendant of StopIteration, so the closing of the ractor will break + * the loops without propagating the error: + * + * r = Ractor.new { 3.times { puts "Received: " + receive } } + * + * loop { r.send "test" } + * puts "Continue successfully" + * + * This will print: + * + * Received: test + * Received: test + * Received: test + * Continue successfully + */ + +/* + * Document-class: Ractor::RemoteError + * + * Raised on attempt to Ractor#take if there was an uncaught exception in the Ractor. + * Its +cause+ will contain the original exception, and +ractor+ is the original ractor + * it was raised in. + * + * r = Ractor.new { raise "Something weird happened" } + * + * begin + * r.take + * rescue => e + * p e # => #<Ractor::RemoteError: thrown by remote Ractor.> + * p e.ractor == r # => true + * p e.cause # => #<RuntimeError: Something weird happened> + * end + * + */ + +/* + * Document-class: Ractor::MovedError + * + * Raised on an attempt to access an object which was moved in Ractor#send or Ractor.yield. + * + * r = Ractor.new { sleep } + * + * ary = [1, 2, 3] + * r.send(ary, move: true) + * ary.inspect + * # Ractor::MovedError (can not send any methods to a moved object) + * + */ + +/* + * Document-class: Ractor::MovedObject + * + * A special object which replaces any value that was moved to another ractor in Ractor#send + * or Ractor.yield. Any attempt to access the object results in Ractor::MovedError. + * + * r = Ractor.new { receive } + * + * ary = [1, 2, 3] + * r.send(ary, move: true) + * p Ractor::MovedObject === ary + * # => true + * ary.inspect + * # Ractor::MovedError (can not send any methods to a moved object) + * + * The class MovedObject is frozen to avoid tampering with it: + * + * class Ractor::MovedObject + * def inspect + * "<MyMovedObject>" + * end + * end + * # FrozenError (can't modify frozen class: Ractor::MovedObject) + */ + +// Main docs are in ractor.rb, but without this clause there are weird artifacts +// in their rendering. +/* + * Document-class: Ractor + * + */ + + void Init_Ractor(void) { diff --git a/ractor.rb b/ractor.rb index 8874107..3a8aa99 100644 --- a/ractor.rb +++ b/ractor.rb @@ -1,34 +1,242 @@ https://github.com/ruby/ruby/blob/trunk/ractor.rb#L1 +# Ractor is a Actor-model abstraction for Ruby that provides thread-safe parallel execution. +# +# To achieve this, ractors severely limit data sharing between different ractors. +# Unlike threads, ractors can't access each other's data, nor any data through variables of +# the outer scope. +# +# # The simplest ractor +# r = Ractor.new {puts "I am in Ractor!"} +# r.take # allow it to finish +# # here "I am in Ractor!" would be printed +# +# a = 1 +# r = Ractor.new {puts "I am in Ractor! a=#{a}"} +# # fails immediately with +# # ArgumentError (can not isolate a Proc because it accesses outer variables (a).) +# +# On CRuby (the default implementation), Global Virtual Machine Lock (GVL) is held per ractor, so +# ractors are performed in parallel without locking each other. +# +# Instead of accessing the shared state, the data should be passed to and from ractors via +# sending and receiving messages, thus making them _actors_ ("Ractor" stands for "Ruby Actor"). +# +# a = 1 +# r = Ractor.new do +# a_in_ractor = receive # receive blocks till somebody will pass message +# puts "I am in Ractor! a=#{a_in_ractor}" +# end +# r.send(a) # pass it +# r.take +# # here "I am in Ractor! a=1" would be printed +# +# There are two pairs of methods for sending/receiving messages: +# +# * Ractor#send and Ractor.receive for when the _sender_ knows the receiver (push); +# * Ractor.yield and Ractor#take for when the _receiver_ knows the sender (pull); +# +# In addition to that, an argument to Ractor.new would be passed to block and available there +# as if received by Ractor.receive, and the last block value would be sent outside of the +# ractor as if sent by Ractor.yield. +# +# A little demonstration on a classic ping-pong: +# +# server = Ractor.new do +# puts "Server starts: #{self.inspect}" +# puts "Server sends: ping" +# Ractor.yield 'ping' # The server doesn't know the receiver and sends to whoever interested +# received = Ractor.receive # The server doesn't know the sender and receives from whoever sent +# puts "Server received: #{received}" +# end +# +# client = Ractor.new(server) do |srv| # The server is sent inside client, and available as srv +# puts "Client starts: #{self.inspect}" +# received = srv.take # The Client takes a message specifically from the server +# puts "Client received from " \ +# "#{srv.inspect}: #{received}" +# puts "Client sends to " \ +# "#{srv.inspect}: pong" +# srv.send 'pong' # The client sends a message specifically to the server +# end +# +# [client, server].each(&:take) # Wait till they both finish +# +# This will output: +# +# Server starts: #<Ractor:#2 test.rb:1 running> +# Server sends: ping +# Client starts: #<Ractor:#3 test.rb:8 running> +# Client received from #<Ractor:#2 rac.rb:1 blocking>: ping +# Client sends to #<Ractor:#2 rac.rb:1 blocking>: pong +# Server received: pong +# +# It is said that Ractor receives messages via the <em>incoming port</em>, and sends them +# to the <em>outgoing port</em>. Either one can be disabled with Ractor#close_incoming and +# Ractor#close_outgoing respectively. +# +# == Shareable and unshareable objects +# +# When the object is sent to and from the ractor, it is important to understand whether the +# object is shareable or not. Shareable objects are basically those which can be used by several +# threads without compromising thread-safety; e.g. immutable ones. Ractor.shareable? allows to +# check this, and Ractor.make_shareable tries to make object shareable if it is not. +# +# Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are +# Ractor.shareable?('foo') #=> false, unless the string is frozen due to # freeze_string_literals: true +# Ractor.shareable?('foo'.freeze) #=> true +# +# ary = ['hello', 'world'] +# ary.frozen? #=> false +# ary[0].frozen? #=> false +# Ractor.make_shareable(ary) +# ary.frozen? #=> true +# ary[0].frozen? #=> true +# ary[1].frozen? #=> true +# +# When a shareable object is sent (via #send or Ractor.yield), no additional processing happens, +# and it just becomes usable by both ractors. When an unshareable object is sent, it can be +# either _copied_ or _moved_. The first is the default, and it makes the object's full copy by +# deep cloning of non-shareable parts of its structure. +# +# data = ['foo', 'bar'.freeze] +# r = Ractor.new do +# data2 = Ractor.receive +# puts "In ractor: #{data2.object_id}, #{data2[0].object_id}, #{data2[1].object_id}" +# end +# r.send(data) +# r.take +# puts "Outside : #{data.object_id}, #{data[0].object_id}, #{data[1].object_id}" +# +# This will output: +# +# In ractor: 340, 360, 320 +# Outside : 380, 400, 320 +# +# (Note that object id of both array and non-frozen string inside array have changed inside +# the ractor, showing it is different objects. But the second array's element, which is a +# shareable frozen string, has the same object_id.) +# +# Deep cloning of the data may be slow, and sometimes impossible. Alternatively, +# <tt>move: true</tt> may be used on sending. This will <em>move</em> the object to the +# receiving ractor, making it inaccessible for a sending ractor. +# +# data = ['foo', 'bar'] +# r = Ractor.new do +# data_in_ractor = Ractor.receive +# puts "In ractor: #{data_in_ractor.object_id}, #{data_in_ractor[0].object_id}" +# end +# r.send(data, move: true) +# r.take +# puts "Outside: moved? #{Ractor::MovedObject === data}" +# puts "Outside: #{data.inspect}" +# +# This will output: +# +# In ractor: 100, 120 +# Outside: moved? true +# test.rb:9:in `method_missing': can not send any methods to a moved object (Ractor::MovedError) +# +# Notice that even +inspect+ (and more basic methods like <tt>__id__</tt>) is inaccessible +# on a moved object. +# +# Besides frozen objects, shareable objects a (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/