Ractor - Ruby’s Actor-like concurrent abstraction¶ ↑
Ractor is designed to provide a parallel execution feature of Ruby without thread-safety concerns.
Summary¶ ↑
Multiple Ractors in an interpreter process¶ ↑
You can make multiple Ractors and they run in parallel.
-
Ractor.new{ expr }creates a newRactorandexpris run in parallel on a parallel computer. -
Interpreter invokes with the first
Ractor(called main Ractor). -
If main
Ractorterminated, all Ractors receive terminate request like Threads (if main thread (first invokedThread), Ruby interpreter sends all running threads to terminate execution). -
Each
Ractorhas 1 or more Threads. -
Threads in a
Ractorshares a Ractor-wide global lock like GIL (GVL in MRI terminology), so they can’t run in parallel (without releasing GVL explicitly in C-level). Threads in different ractors run in parallel. -
The overhead of creating a
Ractoris similar to overhead of oneThreadcreation.
Limited sharing between multiple ractors¶ ↑
Ractors don’t share everything, unlike threads.
-
Most objects are Unshareable objects, so you don’t need to care about thread-safety problem which is caused by sharing.
-
Some objects are Shareable objects.
-
Immutable objects: frozen objects which don’t refer to unshareable-objects.
-
i = 123:iis an immutable object. -
s = "str".freeze:sis an immutable object. -
a = [1, [2], 3].freeze:ais not an immutable object becausearefers unshareable-object[2](which is not frozen). -
h = {c: Object}.freeze:his an immutable object becausehrefersSymbol:cand shareableObjectclass object which is not frozen.
-
-
Class/Module objects
-
Special shareable objects
-
Ractorobject itself. -
And more…
-
Two-types communication between Ractors¶ ↑
Ractors communicate with each other and synchronize the execution by message exchanging between Ractors. There are two message exchange protocols: push type (message passing) and pull type.
-
Push type message passing:
Ractor#send(obj)andRactor.receive()pair. -
Sender ractor passes the
objto the ractorrbyr.send(obj)and receiver ractor receives the message withRactor.receive. -
Sender knows the destination
Ractorrand the receiver does not know the sender (accept all message from any ractors). -
Receiver has infinite queue and sender enqueues the message. Sender doesn’t block to put message into this queue.
-
This type message exchangin is employed by many other Actor-based language.
-
Ractor.receive_if{ filter_expr }is a variant ofRactor.receiveto select a message. -
Pull type communication:
Ractor.yield(obj)andRactor#take()pair. -
Sender ractor declare to yield the
objbyRactor.yield(obj)and receiverRactortake it withr.take. -
Sender doesn’t know a destination
Ractorand receiver knows the senderRactorr. -
Sender or receiver will block if there is no other side.
Copy & Move semantics to send messages¶ ↑
To send unshareable objects as messages, objects are copied or moved.
-
Copy: use deep-copy.
-
Move: move membership.
-
Sender can not access the moved object after moving the object.
-
Guarantee that at least only 1
Ractorcan access the object.
Thread-safety¶ ↑
Ractor helps to write a thread-safe concurrent program, but we can make thread-unsafe programs with Ractors.
-
GOOD: Sharing limitation
-
Most objects are unshareable, so we can’t make data-racy and race-conditional programs.
-
Shareable objects are protected by an interpreter or locking mechanism.
-
BAD: Class/Module can violate this assumption
-
To make it compatible with old behavior, classes and modules can introduce data-race and so on.
-
Ruby programmers should take care if they modify class/module objects on multi
Ractorprograms. -
BAD:
Ractorcan’t solve all thread-safety problems -
There are several blocking operations (waiting send, waiting yield and waiting take) so you can make a program which has dead-lock and live-lock issues.
-
Some kind of shareable objects can introduce transactions (STM, for example). However, misusing transactions will generate inconsistent state.
Without Ractor, we need to trace all of state-mutations to debug thread-safety issues. With Ractor, you can concentrate to suspicious code which are shared with Ractors.
Creation and termination¶ ↑
Ractor.new¶ ↑
-
Ractor.new{ expr }generates anotherRactor.
# Ractor.new with a block creates new Ractor r = Ractor.new do # This block will be run in parallel with other ractors end # You can name a Ractor with `name:` argument. r = Ractor.new name: 'test-name' do end # and Ractor#name returns its name. r.name #=> 'test-name'
Given block isolation¶ ↑
The Ractor execute given expr in a given block. Given block will be isolated from outer scope by Proc#isolate. To prevent sharing unshareable objects between ractors, block outer-variables, self and other information are isolated.
Given block will be isolated by Proc#isolate method (not exposed yet for Ruby users). Proc#isolate is called at