Guild Implementation
Ractor report
Koichi Sasada
Cookpad Inc.

Communication with me• I will check tweets with “#ractor” hashtag on Twitter• I’m at ruby-jp slack workspace, #concurrency

Background
Parallel programming• 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
execution on MRI

Background
Concurrent Thread programming is hard
• Required: Appropriate synchronization forthreads• Threads can share everything• Difficult debugging on non-deterministicnature• Data race
• Race condition• Dead/live locking

Goal:
Easy and Parallel concurrent
programming on Ruby

Our proposal:
Ractor – an Actor-like
concurrent abstraction
Memory model: Limiting object sharing
Good communication API

“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 tochange the class name from Guild to Ractor (Ruby’s Actorlike).

RactorConcepts• Multiple Ractors in an interpreter process• Limited object sharing• Two-types communication between Ractors• Copy & Move semantics to send messages• Details:
https://github.com/ko1/ruby/blob/ractor_parallel/doc/ractor.mdRactorConcept: Parallel execution• Multiple Ractors in an interpreterprocess• Ractors run in parallel• Ractor.new{ expr } makes new Ractor• Ractor has at least 1 Ruby threads, andthreads in a Ractor can not run in parallel(~2.7 compatible)

RactorConcept: Limited object sharing• Strictly separate objects into shareable and unshareable• Unshareable objects – most objects are unshareable• Sharable objects – special objects• Immutable objects (== frozen objects which refer shareable objects)• Class/module objects• Special shareable objects (Ractor objects etc.)• Avoid data races and race conditions• Most of objects are unshareable objects• Shareable objects require appropriate synchronization by theinterpreter or programmer

RactorConcept: Communication/synchronization• Two-types communication between Ractors• Push type: Actor-like send/receive object transferring• Ractor#send(obj) and Ractor.recv pair
• Sender knows receiver ractor (dst.send(obj))• Pull type: Passive message passing style object transferring• Ractor.yield(obj) and Ractor#take pair
• Receiver knows a sender Ractor (src.take)• Copy & Move semantics to send messages• Passed objects will be copied (deep copy)• Move mode is also supported (shallow copy)• After moving, moved objects can’t be touched by sender Ractor

RactorPush/Active message passing• Actor-like communication• Sender knows receiving Ractor• Receiver does not know sending Ractor• Each Ractor has a queue whichconnected to the incoming port.

r1.send(x)Ractor r1

incomingportQueuexf(x)• r1.send(x) enqueues x into the queuex• Queue is unlimited queue, so non blocking• Ractor.recv dequeues queued x• Block if there is no queued objects

Ractor.recv

Pipeline with Traditional Actor model
Ractor r1

Ractor r2
r2.send(f(x))

QueuexQueuemain.send(g(y))

f(x)

f(x)

g(y)

yx

Ractor.recv
or self.recv

Ractor.recv
or self.recv

Ractor mainr1.send(x)Ractor.recv #=> g(f(x))

Queueg(f(x))

RactorPull/Passive message passing• Pull type communication• Sender does not know receiver• Receiver knows sender• Each Ractor has outgoing port.

• Ractor.yield(y) puts y on outgoing port
• r2.take get y from r2’s outgoing port
• These methods will block until anotherRactor take/yield → Rendezvous
synchronization• Block value of given block forRactor.new will be returned by
Ractor.yield implicitly →Ractor#take can supervise theRactor’s liveness.

Ractor r2
Ractor.yield(g(y))

Queueg(y)youtgoing
portPipeline with yield/takeRactor r1

Queuex

Ractor r2

Ractor.yield(f(x))
or self.yield(f(x))

Ractor.yield(g(y))

Queuef(x)g(y)

xyy = r1.take

Ractor.recv
or self.recv

Ractor mainr1.send(x)r2.take #=> g(f(x))

written in code…
r1 = Ractor.new do
x = Ractor.recv
Ractor.yield(f(x))
end
r2 = Ractor.new r1 do |r1|
y = r1.take
Ractor.yield(g(y))
endr1.send(:x)
something()
r2.take #=> g(f(:x))# parallel execution# something()# f() and g()Ractor.yield and Ractor#take
Similarity with Fiber
FiberRactor

f = Fiber.new do
Fiber.yield 1
Fiber.yield 2
3
endf.resume #=> 1
f.resume #=> 2
f.resume #=> 3

r = Rator.new do
Ractor.yield 1
Ractor.yield 2
3
end
r.take #=> 1
r.take #=> 2
r.take #=> 3

RactorRactor#select• Ractor.select(r1, r2, …) will wait from r1, r2, …
• Similar to Go’s select statement• API can be improved more
• For example: Event register approach such as Concurrent-ruby’s channel

Load-balancing multi-workers with abridge Ractor
w1b (bridge)

Ractor.yield(do_task(b.take))w2b.send(task)

Ractor.yield(do_task(b.take))main

Ractor.select(w1, w2)← Exception

Load-balancing multi-workers with abridge Ractorb (bridge)

w1

x1

Ractor.yield(do_task(b.take))

Ractor.yield(do_task(w1.take))w2

x2

Ractor.yield(do_task(b.take))

Ractor.yield(do_task(w2.take))b.send(task)main

Ractor.select(x1, x2)

incoming port/outgoing port
• Two ports• incoming port
• Connected to the incoming queue• Sent message is put to the queue• outgoing port
• Yielded message will be put• They can be closed• close_incoming• Ractor#send raises an error if incoming port is closed• Ractor.recv raise an error if incoming queue is empty and port is closed• close_outgoing• Ractor#take raises an error if outgoing port is closed• Ractor.yield raise an error if outgoing port is closed• When Ractor terminates, both ports are closed automaticallyRactorSupervise Ractors• Ractor#take can supervise Ractors• This method can check return value of Ractor’s given block(Ractor.new{ … }) and Block’s exception.
→ Ractor.select(r1, r2, …) can supervise r1, r2, …

• Compare with other languages• Erlang: link to other process and death event will be notified to thelinked process.
• Go: causes panic on unexpected goroutine’s termination• Ruby (Ractor): Ractor.select(r1, r2, …) can supervise them

Load-balancing multi-workers with abridge Ractorb (bridge)

w1

x1

Ractor.yield(do_task(b.take))

Ractor.yield(do_task(w1.take))w2x2

Ractor.yield(do_task(b.take))

Ractor.yield(do_task(w2.take))b.send(task)main

Ractor.select(x1, x2)← Exception

Advantage of Actor-like based approachcompare with channel-based approach• Easy error detection• If receiver Ractor is died, the error will be occurred• Channel-based approach, we can’t detect destination side-Ractor’s
termination without a trick (ex: close channel’s port in ensure clause)

Ractor main

Ractor r1

r1.send(x)#=> RactorClosedError

Died unexpectedlyRactor
Message passing options• Reference• Shareable objects will be sent by reference (pointer)• Copy: Ractor#send(obj), Ractor.yield(obj)• Objects will be deep copied• Move: Ractor#send(obj, move:true), Ractor.yield(obj, move:true)• Shallow copy
• Long string• IO (File, Socket, …)• Source Ractor can not touch moved objects (will cause exception)

RactorCreation• Ractor.new{ expr } will create new Ractor and execute
expr in parallel with other Ractors• If expr contains reference to the outer-variables, it will beerror• ex) a = [1]; Ractor.new{ p a } #=> Error• Self of given block will be its Ractor object• Block parameters will be sent block arguments• ex)

Ractor.new([1]){|a| p a}#=> r = Ractor.new{a = Ractor.recv; p a}#r.send([1])

RactorSemantic changes• 100% compatible if only main Ractor is used
• Limited to main Ractor (first Ractor)• Global variables $gv• Some gvars ($stdout, …) will be 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Ractor
Example: Web application server
N workers

requesta: accept ractor

b.send(s.accept,move: true)b (bridge)

workerworkerworkerworkert = b.takeworkert = b.takeprime?(t)t = b.takeprime?(t)ta.take= b.takeprime?(t)prime?(t)process(req)mainSupervise: Ractor.select(*workers, a)

responseRactor progress• https://github.com/ko1/ruby/blob/ractor_parallel/✅ Basic Ractor API seems working✅ Ruby apps without Ractor can work (compatible w/ current)⬛ Complex application with Ractor (not enough synchs)⬛ Existing Ruby’s API considerations⬛ C-extension supports⬛ Object passing copy/move support (support only a few types)⬛ Performance tuning• Poor algorithm for Ractor communications• TLS tuning• Object space 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.

RactorEvaluation

Evaluation
Create/Invoke/wait time comparison for 10k
process
ractorthreadfiberproc

WSL2 (Ubuntu 20.04) Ubuntu 18.04
9.608186
36.939180
0.526030
0.259494
0.451909
0.137313
0.022461
0.020944
0.005264
0.003301(sec)

TODO: Make Ractors/threads creation faster as fibers (Ruby 3.1~)
https://gist.github.com/ko1/6257532de84cdb4212581c66415155edEvaluation
Prime number detection• Ractor worker example• Create several worker ractors• Send tasks to them, and aggregate the answer• Task is “Integer#prime?”• 1_000.times{|i| (2**TN + i).prime?}• TN = 10 to 50• TN = 10 → 1024.prime?, 1025.prime?, …
• TN = 50 → 1125899906842624.prime?, 1125899906842625.prime?, …

require 'prime'# parallel program
pipe = Ractor.new doloop do

RN = ARGV.shift.to_i
TN = ARGV.shift.to_i
N = 1_000

Ractor.yield Ractor.recv
end
end
workers = (1..RN).map do

if RN == 0
# sequential program
ans = N.times.map{|i|n = 2 ** TN + i[n, n.prime?]}# pp anselseRactor.new pipe do |pipe|

while n = pipe.take
Ractor.yield [n, n.prime?]
end
end
end
(1..N).each{|i|pipe << 2 ** TN + I}
ans = (1..N).map{_r, (n, b) = Ractor.select(*workers)

https://gist.github.com/ko1/09798986
10f33aef921d864e2f936d0b[n, b]}.sort_by{|(n, b)| n}endRN workersb (bridge)
N.times{|i|b.send(2**TN+i)}

workerworkerworkerworkert = b.takeworkert = b.takeprime?(t)t = b.takeprime?(t)t = b.takeprime?(t)t = b.takeprime?(t)prime?(t)mainRactor.select(workers)

(seq/par)Speedup ractio

Evaluation result
on 4 core 8 threads machine← Slower than sequential

543210Faster than sequential →10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50
TNRN123456781012131415

16

1718

19

20

11

10
FasterExec time (sec)

Evaluation result
on 4 core 8 threads machine

500 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 200 Ractors→ sequential

TN (prime?(2**TN+i)):

Ractor number (RN)
46

47

48

4950

0.15
0.1

FasterExec time (sec)

Evaluation result
on 4 core 8 threads machine

0.05
00 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Ractor number (RN)

TN (prime?(2**TN+i)):

1011121314

Conclusion• Ruby program can run in parallel with Ractor without threadsafety headache• Ractor API and implementation is not matured, but we areworking on it for Ruby 3