Ractor Enhancements,
2024
Koichi Sasada
STORES, Inc.

Today’s topics• Support important features on Ractors• “require”• “timeout”• Memory management issues on Ractors• Future enhancement plans• GC strategy• Proposed APIs

Koichi Sasada• Ruby interpreter developer employed bySTORES, Inc. (2023~) with @mametter• YARV (Ruby 1.9~)• Generational/Incremental GC (Ruby 2.1~)• Ractor (Ruby 3.0~)• debug.gem (Ruby 3.1~)• M:N Thread scheduler (Ruby 3.3~)•…

• Ruby Association Director (2012~)

Glad to be here againthis year!

“Ractor” is• introduced from Ruby 3.0• designed to enable•

parallel computing on Ruby for more
performance on multi-cores• It can make faster applications•

robust concurrent programming• No bugs because of object sharing

Strict Ractor rules to make safer
concurrent programming•

Limiting object sharing features between Ractors• Unshareable and shareable objects• Unshareable objects – most of objects• Sharable objects – some special objects• Immutable objects• Some special objects• Class/Modules• Ractor objects• …

• Constants (and so on) can not get/set unshareable objects by childRactors (= non main Ractors).
• Global variables are not accessible from child Ractors.
•…

Issues on Ractors•

Lack of important features• “require” on child Ractors• “timeout” on child Ractors
•…
•…

•

Performance degression• on memory management•…
•…

Issue of “require”
Child Ractors can not call “require”• Some code requires library in a method on child Ractors• “autoload” case• def foo = (require “foo”; Foo.foo)• “pp” case – require “pp” when the first “pp” is called• So, we need to allow “require” on child Ractors• “require” is prohibited by child Ractors because• It access $LOAD_PATH, $LOADED_FEATURES and so on• The loaded code can define constants with unshareable objects

STR = “str” # set an unshareable object• Complex logics on RubyGems• “require” should be done on main Ractor

“require”
Solution: “require” on main Ractor• Introduce new API “Ractor#interrupt_exec{ expr }”• Run expr on a receiver Ractor’s main thread asynchronously• The block will be translated to the shareable Proc (can’t access outer scopes)• The return value of expr will be ignored so should be sent explicitly• It likes trap handler and sending a signal (therefore it is danger too)

• The main thread will be interrupted any methods such as IO
blocking and so on (like signal handling)• Ractor.main.interrupt_exec{ $g=1 } runs “$g=1” on
the main thread of the main Ractor• Useful to access resources which are limited to main Ractor• Need some overhead to interrupt main threadImplement “require” withRactor#interrupt_exec(Main Ractor)
T/main2. Pause main’s logic
and execute “expr”<expr>

3. Restart main’s logic(Child Ractor)

T/r
1. Ractor.main.interrupt_exec{ expr }(and do not wait for the end of expr)

Implement “require” with
Ractor.require(feature)class Ractor• Ask main Ractor to requiredef self.require(feature)c = Ractor::Channel.new• Call “require” on another
Ractor.main.interrupt_exec do
thread because of recursive
lock (dead lock)Thread.new doc << require(feature)
rescue Exception => e• Caller Ractor should wait forc << ethe result of “requie”
end
end
c.takedef Ractor::Channel.new =end
Ractor.new{loop{ Ractor.yield Ractor.receive }}Implement “require” with
Ractor.require(feature)(Main Ractor)

Restart main’s logic

T/main3. Thread.new
2. Ractor.main.
interrupt_exec(&b)
T/req

4. require(feature)
5. Send a require’s result(Child Ractor)

T/r<Wait for the result>
1. Ractor.require(feature)

6. Restart T/r with the result

Implement “Kernel#require” with
Ractor.require
module Kernel
def require(feature)
return Ractor.require(feature) unless Ractor.main?

# original require on main Ractor
end
end
def Ractor.main? = Ractor.current == Ractor.main

Issue of Ractor supported “require”• Some library override “Kernel#require()”• Rubygems• Bundler•…

• All of them need to insert non-main Ractor guard
def require(feature)
return Ractor.require(feature) unless Ractor.main?
…

• Can we ask to add this line at the beginning of all overriding
definitions?

Issue of Ractor supported “require”
Provide prepended module?
# prepend a module to Kernel can solve itmodule RactorAwareRequire
def require(feature) =Ractor.main? ? Ractor.require(feature) : superend
module Kernel
prepend RactorAwareRequireend# but all ancestors of classes contains it

p ''.class.ancestors# => [String, Comparable, Object, RactorAwareRequire, Kernel, BasicObject]Off-topic
Ractor/Thread#interrupt_exec• This feature is useful for debuggers to stop all threads• Current debugger doesn’t support Ractors → Key feature for it• Current debugger implementation using “line” TracePoint to stop all
threads, but we can’t stop threads running “blocking operation” (I/Owaiting and so on) and can not access to the thread information.

• This feature is danger like trap handlers because it can beinterrupts any code such as cleanup code in ensure• With great power comes great responsibility• Difficult to introduce Ruby’s features? (C-API?)Issue of “timeout”• “timeout” library uses Thead to send asynchronous exception
to the timeout thread• Can not communicate between Ractors
Main Ractor
Register/unregister timeout

Thread 1

TimeoutmonitorthreadChild Ractor

Thread 3
in Ractor 2

Thread 2
Raise Timeout::Error if timeout occurs

Threads can not communicate
between Ractors

“timeout”
Solution 1: Prepare a monitor per each Ractor• Provide monitor threads per a Ractor•Easy implementation with Ractor local variable (30min)•
Need monitor threads
Main Ractor

Thread 1

Thread 2Child Ractor

Timeoutmonitor
thread 1

Thread 3

Thread 4

Timeoutmonitor
thread 2

“timeout”
Solution 2: New communication path
• Use new communication path between Ractors•
1 monitor process in a Ruby (hard for massive Ractors)•Difficult API design
Main Ractor

Thread 1

Thread 2Child RactorTimeoutmonitor
thread 1

Thread 3

Thread 4

Quoted from RubyKaigi 2022 talk: Making *MaNy* threads on Ruby

Handle managed blocking operations{blocking read}
TRQ: [RT2, RT3]

RT1

TRQ: [RT2]

TRQ: [RT2, RT3]

RT3

RT1

NT1

(1) Register RT1 iswaiting for IO

RT2

RT2

TRQ: [RT3]

TRQ: [RT3, RT1]

Continue with IO result(2) Switch to RT2

Timer Threadtime slice
notification

time slice
notification

time slice
notification{IO is ready}Start status:
Ruby threads RT1, RT2, RT3 are there
TRQ (Thread Ready Queue) is [RT2, RT2]

(3) Add RT1 to ready queue→ TRQ: [RT2, RT1]

RT1 can be scheduled early19

“timeout”
Solution 3: Use native timer thread• Use a timer thread for M:N thread scheduler• Timer thread already managing timeout for sleep, etc•
•No need Ruby’s timer thread / Better performance because of C impl.
Need to support not M:N supported platforms
Main Ractor

Child Ractor
Register/unregister timeout

Thread 1

Thread 2

Timernativethread

Raise Timeout::Error if timeout occurs

Thread 3

Thread 4

“timeout”
Solution 3: Use native timer threadmodule Timeout# simplified version
def timeout(sec, exc = Timeout::Error, msg = “…”)
RubyVM.timeout_exec(sec, proc{Thread.current.raise exc, msg}) doyieldendend• RubyVM.timeout_exec will call given Proc when times out by
same mechanism of Ractor#interrupt_exec• Can we introduce general API like Thread.timeout_exec ?

“timeout”
Benchmarking• 99% of “timeout” call does not timeout→ measure: N.times{timeout(1){null_task}}
Execution time of 1M timeout() calls
6••••

seconds543
2

null: no timeout (== tap)
thread: original timeout
thread_ractor: solution 1
native: solution 3• “native” is fastest, but notso fast?

10nullthread

thread_ractor

native“timeout”
Benchmarking• “perf” indicated that accessing hardware timer is an issue• To determine the sleep duration,clock_gettime(CLOCK_MONOTONIC) is used (1M times).

• Use CLOCK_MONOTONIC_CORSE (on Linux) can help
• Faster, but not accurate (up to 4ms error on Ubuntu) and it isenough for this purpose.

“timeout”
Benchmarking
Execution time of 1M timeout() calls
64.80

55.04

seconds

43.52

x2.3
faster

32.18
21
0.09
0
nullthread

thread_ractor

nativenative (coarse)GC Performance issue
Performance survey and no proposals yet

Ring example• Make 50,000 Ractors• Send a message (object) to the next Ractor and measure thetime to go aroundRT0msg

RT1

RTn… many threads …

RT226

Ring example benchmark results
Go around time (sec)
0.8
0.7

0.71

0.69

0.6x6.45
fasterx17.25
faster

0.5
0.4
0.3
0.2

0.11

0.1

0.040M:N disabled

M:N enabled
GC enabled

M:N disabled

M:N enabled
GC disabled

Ring example benchmark results
Creation time (sec)4035.61

35313025x14slower with GC!?

20151052.51

2.19

M:N disabled

M:N enabled

0
M:N disabled

M:N enabled
GC enabled

GC disabled

Data from “Ruby におけるM:Nスレッドの実装”, PPL2024

GC performance issues on Ractors(1) To many GCs because of not enough pages(2) Stopping active Ractors… and more issues?

GC performance issue(1) To many GCs because of not enough pages• Benchmark• Make N ractors or threads and they run the task which create 1Marrays• N.times.map{ Ractor.new{ task } }• GC count and execution time can be expected to be proportional toN at worst• We can expect better speed because of parallel execution

Benchmark result
3

2.5

GC count

3000
2With Ractors
2000

1.5
11000

With Threads 0.50

Execution time (sec)

40000
1 4 7 10 13 16 19 22 25 28 31 34 37 40

R/GC count

T/GC countR/real (sec)T/real (sec)• Execution time isstrongly correlatedwith GC count• Ractors have clearlya larger GC countthan Threads• GC perf. on Ractors
seems slower thanon Threads

Object allocation on Ractors• At the object allocation on a Ractor, the Ractor reserved a heap page
• To remove additional synchronization per object allocation• With 3 pages, Ractor 4 tries to reserve a page, but no page
→ Run GC!! even if there are many unused slots
Object space heap
Ractor 1

Ractor 2
Ractor 3
Ractor 4page

Used slot

Empty slot

Object allocation on Ractors• At the object allocation on a Ractor, the Ractor reserved a heap page• To remove additional synchronization per object allocation• With 3 pages, Ractor 4 tries to reserve a page, but no page
→ Run GC!! even if there are many unused slots
Object space heap
Ractor 1

Ractor 2
Ractor 3
Ractor 4pageGC performance issue(2) Stopping active Ractors• Barrier synchronization for each GC (marking) to make surethe there is no mutation while traversing the whole heap
R1
R2
R3
R4

Need GCRun GC (marking)Pauserequest

Wait allRestartrequestFutureGC tuning• Ractor aware GC tuning• Prepare enough pages for the number of Ractors• Ractor local GC• Need distributed GC techniques• Need more memory vs. single heap
R1R2RRFutureProposed methods in this talk
• Ractor#interrupt_exec (and Thread#interrupt_exec)• Ractor#main?
• Ractor.require(feature)• Ractor::Channel.new• RubyVM.timeout_exec(sec, proc)• And more?

Today’s topics• Support important features on Ractors• “require”• “timeout”• Memory management issues on Ractors• Future enhancement plans• GC strategy• Proposed APIs