I don't understand Ruby 3 Ractor

Congratulations on the scheduled release of Ruby 3.0. 3.0 introduced a feature called Ractor that enables parallel and parallel programming in Ruby. Regarding Ractor, Qiita already has a Good article. However, the level is a little too high for me, who is a completely self-taught amateur in programming, so the following is just a note of a little more rudimentary part for myself. I hope it helps others.

The version of Ruby is ruby ​​3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux].

Out-of-range references

You cannot reference local variables outside of Ractor.

i = 0
Ractor.new {
  i
}.take
#=><internal:ractor>:267:in `new': can not isolate a Proc because it accesses outer variables (i). (ArgumentError)

If you want to pass a local variable, you can pass it as an argument to Ractor.new.

i = 0
Ractor.new(i) {|j|
  puts j    #=>0
}.take

However, you can refer to the constants.

L = 0
Ractor.new {
  puts L    #=>0
}.take

You can also pass an instance of the class.

class Counter
  def initialize
    @value = 0
  end
  attr_reader :value
  
  def inc
    @value += 1
  end
end

c = Counter.new

r = Ractor.new(c) {|counter|
  10.times { counter.inc }
  counter.value
}

puts r.take    #=>10

Mutable object

There was an image that mutable objects (destructible objects) could not be used in Ractor. So are Arrays and Strings bad?

ary = [1, 2, "3"]
str = "Ruby"
Ractor.new(ary, str) {|a, s|
  p a    #=>[1, 2, "3"]
  p s    #=>"Ruby"
}.take

That doesn't seem to be the case. It seems that destructive operation is also possible.

str = "Ruby"
Ractor.new(str) {|s|
  s << "!"
  p s    #=>"Ruby!"
}.take

The following seems to be okay.

str = "Ruby"
p 3.times.map {
  Ractor.new(str) {|s|
    s << ["!", "?"].sample
    s
  }
}.map(&:take)    #=>["Ruby?", "Ruby!", "Ruby?"]
p str            #=>"Ruby"

In other words, it seems that the str ofnew (str)is duplicated and goes into the block variable s. This seems to be the same for instances of homebrew classes.

c = Counter.new

p 4.times.map {
  Ractor.new(c) {|counter|
    counter.inc
    counter.value
  }
}.map(&:take)    #=>[1, 1, 1, 1]

p c.value    #=>0

The array doesn't seem to look like [1, 2, 3, 4]. Instance c is not shared.

class

Classes can be referenced even if they are defined outside.

class A
  def sum(ary)
    ary.sum
  end
end

p Ractor.new {
  a = A.new
  a.sum([*1..10])
}.take    #=>55

So, of course, you can also refer to top-level methods.

def sum(ary)
  ary.sum
end

p Ractor.new {
  sum([*1..10])
}.take    #=>55

Or rather, I wrote "naturally", but what is self in Ractor?

Ractor.new {
 p self                    #=>#<Ractor:#2 ractor_sample.rb:1 running>
 p self.class              #=>Ractor
 p self.class.ancestors    #=>[Ractor, Object, Kernel, BasicObject]
}.take

It seems that the classes defined inside Ractor can also be used outside.

Ractor.new {
  class A
    def rand
      Random.rand(10)
    end
  end
  
  p A.new.rand    #=>7
}.take

p A.new.rand      #=>1

But maybe this just happens to be working ...

Proc, Object#method

It looks like Proc can't be passed to Ractor ... I'm sorry.

f = ->(x) { x * 3 }
Ractor.new(f) {|func|
  (1..4).map(&func)
}.take
#=><internal:ractor>:267:in `new': allocator undefined for Proc (TypeError)

Then this is also useless, isn't it?

def f(x) = x * 3
Ractor.new(method(:f)) {|func|
  (1..4).map(&func)
}.take
#=><internal:ractor>:267:in `new': allocator undefined for Method (TypeError)

Do you want to give up ...

def f(x) = x * 3
Ractor.new {
  p (1..4).map(&method(:f))    #=>[3, 6, 9, 12]
}.take

Well, I don't think it's necessary to force it to look like functional programming in Ruby, but it's a bit disappointing. By the way, I used "Endless method definition" (1 line def) here.

Pull type communication

Use Ractor.yield and Ractor # take. Ractor.yield waits for an external Ractor # take and then takes the value. The return value of Ractor is automatically Ractor.yield. Error if you take extra. Will Ractor.yield that is not taken be thrown away?

r = Ractor.new do
  Ractor.yield 1
  Ractor.yield 2
  3
end

p r.take    #=>1
sleep(1)
p r.take    #=>(1 second later) 2
p r.take    #=>3

p r.take    #=><internal:ractor>:694:in `take': The outgoing-port is already closed (Ractor::ClosedError)

If you take it before Ractor.yield, wait until it is yielded.

r = Ractor.new do
  sleep(1)
  Ractor.yield 1
end

p r.take    #=>(1 second later) 1

That is, it will not be evaluated until it is needed (Ractor # taken). It's kind of like "lazy evaluation".

Push type communication

Use Ractor # send to plunge into a Queue in Ractor and Ractor.receive to retrieve it from the Queue. Block if the Queue is empty.

r = Ractor.new do
  loop do
    p Ractor.receive
  end
end

r.send(1)
r.send(2)
sleep(1)
r.send(3)

r.take

Doing this will print 1, 2 first, then 3 after 1 second and then freeze. Since there is a Queue inside, you can store the input by Ractor # send.

Push + Pull

Example of concurrency

r = Ractor.new do
  a = Ractor.receive
  #(Processing using something a)
  #(Return value)
end

#r and do_Process something in parallel
r.send(data)
do_something
result = r.take

I wonder if r can be reused this way.

r = Ractor.new do
  loop do
    a = Ractor.receive
    #(Processing using something a)
    Ractor.yield result    #Return value
  end
end

What is this doing

It's a primality test from 1 to 1000 ...

require 'prime'

N = 1000
RN = 10

pipe = Ractor.new do
  loop do
    Ractor.yield Ractor.receive
  end
end

workers = (1..RN).map do
  Ractor.new(pipe) do |pipe|
    loop do
      n = pipe.take
      Ractor.yield [n, n.prime?]
    end
  end
end

(1..N).each { |i| pipe.send(i) }

pp (1..N).map {
  r, (n, b) = Ractor.select(*workers)
  [n, b]
}.sort_by { |(n, b)| n }

You can pass another Ractor to the Ractor.

pipeIspipe.sendStore what was done in the internal Queue,pipe.takeがあるまで待って流します。ただここでIs先にworkerIn the definition ofpipe.takeBecause it has been done laterpipe.sendWait for youpipe.takeすることになります。これIsどちらが先でもいいので、(1..N).each { |i| pipe.send(i) }ToworkerYou can also bring it before the definition of.

A total of RNs are created for workers. In other words, it is parallel processing of 1 pipe + RN.

N data is poured into pipe. It is distributed to RN number of workers and processed.

Each worker waits for the result Ractor.yield when it's done. Receive them with Ractor.select (* workers) in the order in which they were created. The passed worker will repeat the same process with loop.

It's kind of like "lazy evaluation".

Finally

The following was very helpful.

Please point out any mistakes.

Recommended Posts

I don't understand Ruby 3 Ractor
Java concurrency I don't understand
I started Ruby
I don't understand because I translate Serialize into serialization
Multi-stage selection (Ruby Ractor)
I tried DI with Ruby
Personal memo Progate Ruby I (2)
Personal memo Progate Ruby I (1)
I tried Jets (ruby serverless)
ruby exercise memo I (puts)
[Ruby] Maybe you don't really understand? [Difference between class and module]
I don't understand the devise_parameter_sanitizer method, so I'll output it here.
I checked this
Java concurrency I don't understand
I don't understand Ruby 3 Ractor
I tried to understand nil guard
Ruby: I made a FizzBuzz program!
I tried to understand nil guard
Others (CDK calculation: I don't know ...)
<First post> I started studying Ruby