RUBYCONF 2020

RactorDemonstration
Koichi Sasada(from Japan)

Cookpad Inc.

Ask Koichi• I will check tweets with “#ractor” hashtag on twitter so if you havea question, please tell me.
• You can access this presentation slies on:
https://www.atdot.net/~ko1/activities/

Background
Parallelprogramming• Parallel execution on Multi-core CPUs is important• Multi-process programming is not easy
• Hard to communicate• Hard to control resource consumption• Multi-thread doesn’t support parallel executionon MRI

Demonstration
MRI can not utilize CPUs with threads# make 20 threads running busy loop
(1..20).each do# each threads run busy loop
Thread.new{ loop{} }endIntel(R) Xeon(R) CPU E5-2630 v4
10 cores x 2 HT x2 CPUs = 40 logical CPUs

Background
Concurrent Threadprogramming is hard

• Appropriate synchronization is needed• Threads can share everything• Critical bugs w/o synchronization• Data race / race condition• Dead/live locking• Difficult debugging on nondeterministic nature• Difficult to tune the performance on
fine-grained synchronizations

Demonstration
Threads require synchronization# two threads increment numbers
counter = 0
get = proc{ counter }(1..2).map do |i|
Thread.new do
1_000_000.times{ counter = get.call + 1 }endend.each{|t| t.join}
p counter #=> 2_000_000 is expected,# but 1709078, 1712839, …

Demonstration
Threads require synchronization# two threads increment numbers
counter = 0; m = Mutex.new
get = proc{ counter }(1..2).map do |i|
Thread.new do
1_000_000.times{ m.synchronize{ counter = get.call + 1 }}end
end.each{|t| t.join}
p counter #=> 2_000_000

Goal:
Easy and Parallel concurrent
programming on Ruby

Our proposal:

Ractoran Actor-like
concurrent abstraction

Limited object sharing
between ractors
with inter-ractor
communication

“Guild” → “Ractor”• Basic concept was proposed with “Guild” code name at RubyKaigi 2016 and 2018
• http://rubykaigi.org/2016/presentations/ko1.html• https://rubykaigi.org/2018/presentations/ko1.html• With Matz, we discussed the name of Guild and decided to change the class namefrom Guild to Ractor (Ruby’s Actor-like).

RactorConcepts• Run multiple ractors in parallel• Limited object sharing• Building ractors network with push/pull
types communication• Sending objects with copy/move• Ractor’s specificaiton:
https://github.com/ko1/ruby/blob/ractor_parallel/doc/ractor.mdRactors concept

Run multiple ractors in parallel• Multi-Ractors in one process• Ractor.new{ expr } makesa new Ractor• Ractor and thread• A process has at least 1 ractor• A ractor has at least 1 thread• Threads in a Ractor can not run inparallel (~2.7 compatible)

Demonstration
Making a ractorr = Ractor.new dop self #=> #<Ractor:#2 t.rb:1 running>end#=> warning: Ractor is experimental, and the behavior may change in futureversions of Ruby! Also there are many implementation issues.

#=> Ractor is an experimental feature on Ruby 3.0#Specifications can be changed with your voice!

Demonstration
Multiple Ractors run simultaneously# make 20 ractors running busy loop
(1..20).map do# each threads run busy loop
Ractor.new{ loop{} }end

Demonstration
Ractor creation and waiting for the result
require "prime"

r = Ractor.new( 2**61 - 1 ) do |i|i.prime?
end
p r.take # You can get the result#=> trueDemonstration
Heavy numeric calculation# Sequential thread(t1 = Time.now
n1 = 2**61 - 1
n2 = 2**61 + 15
[n1.prime?, n2.prime?])p Time.now - t1

# multi-ractor(t1 = Time.now
n1 = 2**61 - 1
n2 = 2**61 + 15
r1 = Ractor.new(n1){|p1| p1.prime? }r2 = Ractor.new(n2){|p2| p2.prime? }[r1.take, r2.take])p Time.now - t1

Demonstration
Object creation on ractors is slower yet

def task =1_000_000.times{''}(t1 = Time.now
2.times{task}Time.now - t1)

0.2 seconds(t1 = Time.now
r1 = Ractor.new{ task }r2 = Ractor.new{ task }r1.take; r2.take
Time.now - t1)

0.7 seconds

Ractor’s concept

Limited object sharing• The biggest difficulties of thread programming is shared everything• Most of objects are not shared with multiple ractors• String, Array, Hash, User defined objects…
• You can not introduce synchronization bugs because they are not needed on Ractors!

Ractor’s concept

Shareable objects• Classes/modules• Immutable objects (deeply frozen objects)• Ractor.make_sharable(obj) makes obj recursively frozen• Special shared objects• Ractor objects• Transactional variables (not introduced yet)
• Sharable Proc
• …

Ractor’s concept

Building ractors networkwith push/pull types communication• Make a program with multi-ractors network• Ractors can wait for the message arrival→ We can manage the control flow
• Two types communication APIs
• Push type (Ractor#send / Ractor.receive)• Pull type (Ractor.yield / Ractor#take)Ractor’s idea
Push type communication
copy object
Ractor: r1
r2.send(obj)

obj

obj

“r1 sends a message to r2”

Send a message to r2,and return “send” immediately

Ractor: r2

Ractor.receive

Wait the message,
and return with areceipt (copied) object

Message passing / Actor style communication

Demonstration
Sending an object
main:
r1.send(“hello”)

Ractor:main

r1 = Ractor.new do
obj = Ractor.receive
id:60
p obj.object_id #=> 80
endobj = "hello"p obj.object_id#=> 60r1.send(obj)r1.take # wait for the execution“hello”

r1:
Ractor.receive

Ractor: r1“hello”

id:80

copy

Demonstration
Sending a nested object
main:
r1.send([[1], 2])

Ractor:main

r1 = Ractor.new do
obj = Ractor.receive
id:60
p obj[0].object_id #=> 80
end
obj = [[1], 2] # nested arrayp obj[0].object_id#=> 60r1.send(obj)r1.take # wait for the execution[a, 2]

a:[1]r1:Ractor.receive

deep copy

Ractor: r1[a, 2]
a:[1]

id:80

Ractor’s idea
Incoming queue and incoming port
Ractor r2
incoming
port

r2.send(o1)Ractor: r3
r2.send(o2)Queueinfinite size

Ractor: r1

o2
o1

o1

Any ractors can send to r2
Ractor.receiveDemonstration
Send to closed port

r1 = Ractor.new doclose_incoming
end
sleep 0.1 # wait for close

r1.send(1)Ractor:main

main:
r1.send(obj)

Ractor: r1
r1:
close_incoming

NOTE:
Incoming port will be closed
when the ractor is finished.
→ You can not send to a dead ractor#=> The incoming-port is already closed (Ractor::ClosedError)

Ractor’s idea
Pull type communication
copy object
Ractor: r1

obj

Ractor.yield(obj)
Wait for taking ractor

obj

Ractor: r2

r1.take“r2 takes a message from r1”

Wait of the message,
and return with areceipt (copied) object

Rendezvous style communication

Demonstration
Pull from a ractor
Ractor:main

main:
r1.taker1:
Ractor.yield 0
Ractor.yield 1
Ractor.yield 2
terminate with :fin

Ractor: r1

r1 = Ractor.new do
3.times{|i| Ractor.yield i }:fin # the result of block will be yieldedend4.times{ p r1.take } #=> 0 1 2 :finRactor’s idea
Yield/take via outgoing port
Ractor r1

Ractor r2

Ractor.yield(f(x))

Queuexf(x)outgoingport

Ractor.yield(g(y))

Queueg(y)yxRactor.receivey = r1.take

Demonstration
Pipeline parallel
Ractor: r2

Ractor: r1

def task(n) = n + 1

main: r1.send

r1 = Ractor.new doloop{ Ractor.yield(task(Ractor.receive)) }end
r2 = Ractor.new r1 do |r1|loop{ Ractor.yield(task(r1.take)) }end

r1.send(1)p r2.take #=> 3Ractor:
main

main: r2.take

Demonstration
Close outgoing port
Ractor:main

r1 = Ractor.new doclose_outgoingend
r1.takemain:
r1.take

Ractor: r1
r1:
close_outgoing

NOTE:
Outgoing port will be closed
when the ractor is finished.
→ You can not take from a dead ractor#=> `take': The outgoing-port is already closed (Ractor::ClosedError)

Demonstration
Taken by multi-ractors
r1: main.take

main = Ractor.current
r1 = Ractor.new main do |main|
p r1: main.takeend
r2 = Ractor.new main do |main|p r2: main.takeend
Ractor.yield(:messaeg)#=> r1 or r2 take a message

Ractor:main
main: Ractor.yield obj

Ractor: r1
r2: main.take

Ractor: r2

Ractor’s idea
Load balancing with a bridge ractor

Ractor: main
bridge.send(req)Ractor: bridge
loop doRactor.yield(Ractor.receive)end

Ractor: r1
Ractor: r2

Ractor: r3
A sent “obj” will be received
by idle ractor r1, r2 or r3

Ractor’s idea
Taking from multiple ractors with “select”
Ractor: r1

Ractor: main
Ractor: r2
Ractor.select(r1, r2, r3)
Ractor: r3

Ractor’s idea
Get the results from worker poolr1: bridge.take

main: bridge.send(req)Ractor: r1
r2: bridge.take

Ractor: main

Ractor: bridge

Ractor: r2
r3: bridge.take

Ractor: r3

main: Ractor.select(r1, r2, r3)

Demonstration
Workers poolmain: bridge.send(req)Ractor: main

Ractor: r1
Ractor: bridge

require "prime"
def task(n) = [n, n.prime?]
bridge = Ractor.new{loop{Ractor.yield Ractor.receive}}
workers = (1..3).map{|i|Ractor.new(bridge, name: "r#{i}"){|b|loop{Ractor.yield task(b.take)}}}3.times{|i| bridge.send 11 + i}
# send 3 requests
3.times{ p Ractor.select(*workers) } # take 3 responses#=> [#<Ractor:#3 r1 ...>, [11, true]]#=> [#<Ractor:#3 r1 ...>, [13, true]]#=> [#<Ractor:#4 r2 ...>, [12, false]]

Ractor: r2
Ractor: r3

Ractor’s idea
More complex ractor network

Ractor: main

Ractor: bridge

Ractor: r1

Ractor: r4

Ractor: r2

Ractor: r5

Ractor: r3

Ractor: r6

main: Ractor.select(r4, r5, r6)
r1~r6 run their task in parallel

Ractor’s idea
Exception propagation
Ractor: r1
raise SomeError

r2: r1.take

Ractor: r2

Exception will be propagated

r1.take

A ractor finished with an error

r1.take raises a RemoteError(like Thread#join)

Demonstration
Exception propagation
Ractor:main

Ractor: r1

r1 = Ractor.new do
1+"2" #=> TypeError
main: r1.take
end
begin
r1.takerescue Ractor::RemoteError => e
p e#=> #<Ractor::RemoteError: thrown by remote Ractor.>p e.cause #=> #<TypeError: String can't be coerced into Integer>
p e.ractor #=> #<Ractor:#2 … terminated>
endRactor’s idea
Error recovery on the ractor network
r4: r1.take

main: bridge.send(req)Ractor: r1

Ractor: r4
r5: r2.take

Ractor: main

Ractor: bridge

Ractor: r2

Ractor: r5
r6: r3.take

Ractor: r3
main: Ractor.select(r4, r5, r6) #=> Error

Ractor: r6

If r2 terminated with an error, the error
will be propagated to r5 and main.
main can restart r2 and r5 if needed.

Importantsemantic changes• Completely compatible with Ruby 2.x if there is
only the main Ractor (first created Ractor)• Limited to the main Ractor• Global variables $gv• Some ($stdout, $$ …) are Ractor local• Class variables @@cv
• Instance variables of shareable objects• Ivars of class/module are prohibited• Constants refer to unshareable objects• C = [1] is prohibited• For Ractor programming, many modifications
are needed• We are discussing how to provide an easy way tomake Ractor libraries

Ractor implementation progress✅ Basic Ractor APIs
⬛ Advanced APIs
✅ Ruby apps without Ractor⬛ Complex application with Ractor (not enough synchronizations)⬛ Existing Ruby’s API considerations⬛ C-extension supports⬛ Performance tuning$ ./miniruby -e Ractor.new{}<internal:ractor>:37: warning: Ractor is experimental,
and the behavior may change in future versions of Ruby!
Also there are many implementation issues.

More interesting features…

• Sending message with copying/moving semantics• Shareable “Proc” semantics• Ractor-safe and efficient internal implementations• …

Reference• Ractor specification:
https://github.com/ko1/ruby/blob/ractor_parallel/doc/ractor.md• RubyKaigi 2020 takeout presentation (“Ractor report”)• More detailed data and programming models• https://www.youtube.com/watch?v=40t8EPpnujg• http://www.atdot.net/~ko1/activities/2020_rubykaigi.pdf

Conclusion• Ruby can run in parallel with Ractor without
thread-safety headache• You can enjoy ractor programming on Ruby 3.0• Ractor API and implementation is not matured• Ruby 3.0 is a Ractor preview release• Your comments on your experience are welcome 😍

RUBYCONF 2020Ractor
Demonstration
Koichi Sasada
Cookpad Inc.