Class: Ractor
Relationships & Source Files | |
Namespace Children | |
Classes:
| |
Exceptions:
| |
Inherits: | Object |
Defined in: | ractor.rb, ractor.c |
Overview
Ractor is an Actor-model abstraction for ::Ruby
that provides thread-safe parallel execution.
.new makes a new Ractor, which can run in parallel.
# The simplest ractor
r = Ractor.new {puts "I am in Ractor!"}
r.join # wait for it to finish
# Here, "I am in Ractor!" is printed
Ractors do not share all objects with each other. There are two main benefits to this: across ractors, thread-safety concerns such as data-races and race-conditions are not possible. The other benefit is parallelism.
To achieve this, object sharing is limited across ractors. For example, unlike in threads, ractors can’t access all the objects available in other ractors. Even objects normally available through variables in the outer scope are prohibited from being used across ractors.
a = 1
r = Ractor.new {puts "I am in Ractor! a=#{a}"}
# fails immediately with
# ArgumentError (can not isolate a Proc because it accesses outer variables (a).)
The object must be explicitly shared:
a = 1
r = Ractor.new(a) { |a1| puts "I am in Ractor! a=#{a1}"}
On CRuby (the default implementation), Global Virtual Machine Lock (GVL) is held per ractor, so ractors can perform in parallel without locking each other. This is unlike the situation with threads on CRuby.
Instead of accessing shared state, objects should be passed to and from ractors by sending and receiving them as messages.
a = 1
r = Ractor.new do
a_in_ractor = receive # receive blocks until somebody passes a message
puts "I am in Ractor! a=#{a_in_ractor}"
end
r.send(a) # pass it
r.join
# Here, "I am in Ractor! a=1" is printed
In addition to that, any arguments passed to .new are passed to the block and available there as if received by .receive, and the last block value can be received with #value.
Shareable and unshareable objects
When an object is sent to and from a ractor, it’s important to understand whether the object is shareable or unshareable. Most Ruby objects are unshareable objects. Even frozen objects can be unshareable if they contain (through their instance variables) unfrozen objects.
Shareable objects are those which can be used by several threads without compromising thread-safety, for example numbers, true
and false
. .shareable? allows you to check this, and .make_shareable tries to make the object shareable if it’s not already, and gives an error if it can’t do it.
Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are shareable
Ractor.shareable?('foo') #=> false, unless the string is frozen due to # frozen_string_literal: true
Ractor.shareable?('foo'.freeze) #=> true
Ractor.shareable?([Object.new].freeze) #=> false, inner object is unfrozen
ary = ['hello', 'world']
ary.frozen? #=> false
ary[0].frozen? #=> false
Ractor.make_shareable(ary)
ary.frozen? #=> true
ary[0].frozen? #=> true
ary[1].frozen? #=> true
When a shareable object is sent (via #send or Ractor.yield
), no additional processing occurs on it. It just becomes usable by both ractors. When an unshareable object is sent, it can be either copied or moved. The first is the default, and it copies the object fully by deep cloning (Object#clone) the non-shareable parts of its structure.
data = ['foo', 'bar'.freeze]
r = Ractor.new do
data2 = Ractor.receive
puts "In ractor: #{data2.object_id}, #{data2[0].object_id}, #{data2[1].object_id}"
end
r.send(data)
r.take
puts "Outside : #{data.object_id}, #{data[0].object_id}, #{data[1].object_id}"
This will output something like:
In ractor: 340, 360, 320
Outside : 380, 400, 320
Note that the object ids of the array and the non-frozen string inside the array have changed in the ractor because they are different objects. The second array’s element, which is a shareable frozen string, is the same object.
Deep cloning of objects may be slow, and sometimes impossible. Alternatively, move: true
may be used during sending. This will move the unshareable object to the receiving ractor, making it inaccessible to the sending ractor.
data = ['foo', 'bar']
r = Ractor.new do
data_in_ractor = Ractor.receive
puts "In ractor: #{data_in_ractor.object_id}, #{data_in_ractor[0].object_id}"
end
r.send(data, move: true)
r.take
puts "Outside: moved? #{Ractor::MovedObject === data}"
puts "Outside: #{data.inspect}"
This will output:
In ractor: 100, 120
Outside: moved? true
test.rb:9:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)
Notice that even #inspect (and more basic methods like __id__
) is inaccessible on a moved object.
::Class
and ::Module
objects are shareable so the class/module definitions are shared between ractors. Ractor objects are also shareable. All operations on shareable objects are thread-safe, so the thread-safety property will be kept. We can not define mutable shareable objects in ::Ruby
, but C extensions can introduce them.
It is prohibited to access (get) instance variables of shareable objects in other ractors if the values of the variables aren’t shareable. This can occur because modules/classes are shareable, but they can have instance variables whose values are not. In non-main ractors, it’s also prohibited to set instance variables on classes/modules (even if the value is shareable).
class C
class << self
attr_accessor :tricky
end
end
C.tricky = "unshareable".dup
r = Ractor.new(C) do |cls|
puts "I see #{cls}"
puts "I can't see #{cls.tricky}"
cls.tricky = true # doesn't get here, but this would also raise an error
end
r.take
# I see C
# can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
Ractors can access constants if they are shareable. The main Ractor is the only one that can access non-shareable constants.
GOOD = 'good'.freeze
BAD = 'bad'.dup
r = Ractor.new do
puts "GOOD=#{GOOD}"
puts "BAD=#{BAD}"
end
r.take
# GOOD=good
# can not access non-shareable objects in constant Object::BAD by non-main Ractor. (NameError)
# Consider the same C class from above
r = Ractor.new do
puts "I see #{C}"
puts "I can't see #{C.tricky}"
end
r.take
# I see C
# can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
See also the description of # shareable_constant_value
pragma in Comments syntax
explanation.
Ractors vs threads
Each ractor has its own main ::Thread
. New threads can be created from inside ractors (and, on CRuby, they share the GVL with other threads of this ractor).
r = Ractor.new do
a = 1
Thread.new {puts "Thread in ractor: a=#{a}"}.join
end
r.take
# Here "Thread in ractor: a=1" will be printed
Note on code examples
In the examples below, sometimes we use the following method to wait for ractors that are not currently blocked to finish (or to make progress).
def wait
sleep(0.1)
end
It is **only for demonstration purposes** and shouldn’t be used in a real code. Most of the time, #take is used to wait for ractors to finish.
Reference
See design doc
for more details.
Class Attribute Summary
-
.main
readonly
returns main ractor.
-
.main? ⇒ Boolean
readonly
return true if the current ractor is main ractor.
Class Method Summary
-
.[](sym)
get a value from ractor-local storage of current
Ractor
. -
.[]=(sym, val)
set a value in ractor-local storage of current
Ractor
. -
.count
Returns the number of Ractors currently running or blocking (waiting).
-
.current
Returns the currently executing
Ractor
. -
.make_shareable(obj, copy: false) ⇒ shareable_obj
Make
obj
shareable between ractors. -
.new(*args, name: nil) {|*args| ... } ⇒ Ractor
constructor
Create a new Ractor with args and a block.
-
.receive ⇒ Object
(also: .recv)
Receive a message from the default port.
-
.recv
Alias for .receive.
-
.select(*ports) ⇒ Array
TBD.
-
.shareable?(obj) ⇒ Boolean
Checks if the object is shareable by ractors.
-
.store_if_absent(key)
If the corresponding value is not set, yield a value with init_block and store the value in thread-safe manner.
-
._require(feature)
Internal use only
internal method.
-
._activated
private
Internal use only
internal method that is called when the first “Ractor.new” is called.
Instance Method Summary
-
#<<
Alias for #send.
-
#[](sym)
get a value from ractor-local storage for current
Ractor
Obsolete and use .[] instead. -
#[]=(sym, val)
set a value in ractor-local storage for current
Ractor
Obsolete and use .[]= instead. -
#close
Closes default_port.
-
#default_port ⇒ port object
return default port of the
Ractor
. - #inspect (also: #to_s)
-
#join ⇒ self
Wait for the termination of the
Ractor
. -
#monitor(port) ⇒ self
Register port as a monitoring port.
-
#name
The name set in .new, or
nil
. -
#recv
Alias for #receive.
-
#send(msg) ⇒ self
(also: #<<)
It is equivalent to default_port.send(msg).
-
#take
keep it for compatibility.
-
#to_s
Alias for #inspect.
-
#unmonitor(port) ⇒ self
Unregister port from the monitoring ports.
-
#value ⇒ Object
Waits for
ractor
to complete, using #join, and return its value or raise the exception which terminated theRactor
. -
#receive
(also: #recv)
private
same as .receive
Constructor Details
.new(*args, name: nil) {|*args| ... } ⇒ Ractor
Create a new Ractor with args and a block.
The given block (Proc) will be isolated (can’t access any outer variables). self
inside the block will refer to the current Ractor.
r = Ractor.new { puts "Hi, I am #{self.inspect}" }
r.take
# Prints "Hi, I am #<Ractor:#2 test.rb:1 running>"
Any args
passed are propagated to the block arguments by the same rules as objects sent via #send/Ractor.receive. If an argument in args
is not shareable, it will be copied (via deep cloning, which might be inefficient).
arg = [1, 2, 3]
puts "Passing: #{arg} (##{arg.object_id})"
r = Ractor.new(arg) {|received_arg|
puts "Received: #{received_arg} (##{received_arg.object_id})"
}
r.take
# Prints:
# Passing: [1, 2, 3] (#280)
# Received: [1, 2, 3] (#300)
Ractor’s #name can be set for debugging purposes:
r = Ractor.new(name: 'my ractor') {}; r.take
p r
#=> #<Ractor:#3 my ractor test.rb:1 terminated>
# File 'ractor.rb', line 231
def self.new(*args, name: nil, &block) b = block # TODO: builtin bug raise ArgumentError, "must be called with a block" unless block if __builtin_cexpr!("RBOOL(ruby_single_main_ractor)") Kernel.warn("Ractor is experimental, and the behavior may change in future versions of Ruby! " \ "Also there are many implementation issues.", uplevel: 0, category: :experimental) end loc = caller_locations(1, 1).first loc = "#{loc.path}:#{loc.lineno}" __builtin_ractor_create(loc, name, args, b) end
Class Attribute Details
.main (readonly)
returns main ractor
# File 'ractor.rb', line 481
def self.main __builtin_cexpr! %q{ rb_ractor_self(GET_VM()->ractor.main_ractor); } end
.main? ⇒ Boolean
(readonly)
return true if the current ractor is main ractor
# File 'ractor.rb', line 488
def self.main? __builtin_cexpr! %q{ RBOOL(GET_VM()->ractor.main_ractor == rb_ec_ractor_ptr(ec)) } end
Class Method Details
.[](sym)
get a value from ractor-local storage of current Ractor
# File 'ractor.rb', line 453
def self.[](sym) Primitive.ractor_local_value(sym) end
.[]=(sym, val)
set a value in ractor-local storage of current Ractor
# File 'ractor.rb', line 458
def self.[]=(sym, val) Primitive.ractor_local_value_set(sym, val) end
._activated (private)
internal method that is called when the first “Ractor.new” is called
._require(feature)
internal method
# File 'ractor.rb', line 495
def self._require feature # :nodoc: if main? super feature else Primitive.ractor_require feature end end
.count
# File 'ractor.rb', line 260
def self.count __builtin_cexpr! %q{ ULONG2NUM(GET_VM()->ractor.cnt); } end
.current
Returns the currently executing Ractor
.
Ractor.current #=> #<Ractor:#1 running>
# File 'ractor.rb', line 246
def self.current __builtin_cexpr! %q{ rb_ractor_self(rb_ec_ractor_ptr(ec)); } end
.receive ⇒ Object Also known as: .recv
Receive a message from the default port.
# File 'ractor.rb', line 312
def self.receive Ractor.current.default_port.receive end
.recv
Alias for .receive.
# File 'ractor.rb', line 317
alias recv receive
.select(*ports) ⇒ Array
TBD
# File 'ractor.rb', line 271
def self.select(*ports) raise ArgumentError, 'specify at least one ractor or `yield_value`' if ports.empty? monitors = {} # Ractor::Port => Ractor ports = ports.map do |arg| case arg when Ractor port = Ractor::Port.new monitors[port] = arg arg.monitor port port when Ractor::Port arg else raise ArgumentError, "should be Ractor::Port or Ractor" end end begin result_port, obj = __builtin_ractor_select_internal(ports) if r = monitors[result_port] [r, r.value] else [result_port, obj] end ensure # close all ports for join monitors.each do |port, r| r.unmonitor port port.close end end end
.store_if_absent(key)
If the corresponding value is not set, yield a value with init_block and store the value in thread-safe manner. This method returns corresponding stored value.
(1..10).map{
Thread.new(it){|i|
Ractor.store_if_absent(:s){ f(); i }
#=> return stored value of key :s
}
}.map(&:value).uniq.size #=> 1 and f() is called only once
# File 'ractor.rb', line 476
def self.store_if_absent(sym) Primitive.ractor_local_value_store_if_absent(sym) end
Instance Method Details
#<<
Alias for #send.
# File 'ractor.rb', line 335
alias << send
#[](sym)
get a value from ractor-local storage for current Ractor
Obsolete and use .[] instead.
# File 'ractor.rb', line 436
def [](sym) if (self != Ractor.current) raise RuntimeError, "Cannot get ractor local storage for non-current ractor" end Primitive.ractor_local_value(sym) end
#[]=(sym, val)
set a value in ractor-local storage for current Ractor
Obsolete and use .[]= instead.
# File 'ractor.rb', line 445
def []=(sym, val) if (self != Ractor.current) raise RuntimeError, "Cannot set ractor local storage for non-current ractor" end Primitive.ractor_local_value_set(sym, val) end
#close
Closes default_port. Closing port is allowed only by the ractor which creates this port. So this close method also allowed by the current Ractor
.
# File 'ractor.rb', line 366
def close default_port.close end
#default_port ⇒ port
object
return default port of the Ractor
.
# File 'ractor.rb', line 528
def default_port __builtin_cexpr! %q{ ractor_default_port_value(RACTOR_PTR(self)) } end
#inspect Also known as: #to_s
[ GitHub ]# File 'ractor.rb', line 337
def inspect loc = __builtin_cexpr! %q{ RACTOR_PTR(self)->loc } name = __builtin_cexpr! %q{ RACTOR_PTR(self)->name } id = __builtin_cexpr! %q{ UINT2NUM(rb_ractor_id(RACTOR_PTR(self))) } status = __builtin_cexpr! %q{ rb_str_new2(ractor_status_str(RACTOR_PTR(self)->status_)) } "#<Ractor:##{id}#{name ? ' '+name : ''}#{loc ? " " + loc : ''} #{status}>" end
#join ⇒ self
#monitor(port) ⇒ self
Register port as a monitoring port. If the ractor terminated, the port received a ::Symbol
object. :exited
will be sent if the ractor terminated without an exception. :aborted
will be sent if the ractor terminated with a exception.
r = Ractor.new{ some_task() }
r.monitor(port = Ractor::Port.new)
port.receive #=> :exited and r is terminated
r = Ractor.new{ raise "foo" }
r.monitor(port = Ractor::Port.new)
port.receive #=> :terminated and r is terminated with an exception "foo"
# File 'ractor.rb', line 601
def monitor port __builtin_ractor_monitor(port) end
#name
The name set in .new, or nil
.
# File 'ractor.rb', line 350
def name __builtin_cexpr! %q{RACTOR_PTR(self)->name} end
#receive (private) Also known as: #recv
same as .receive
# File 'ractor.rb', line 321
private def receive default_port.receive end
#recv
Alias for #receive.
# File 'ractor.rb', line 324
alias recv receive
#send(msg) ⇒ self
Also known as: #<<
It is equivalent to default_port.send(msg)
# File 'ractor.rb', line 331
def send(...) default_port.send(...) self end
#take
keep it for compatibility
#to_s
Alias for #inspect.
# File 'ractor.rb', line 347
alias to_s inspect
#unmonitor(port) ⇒ self
Unregister port from the monitoring ports.
# File 'ractor.rb', line 611
def unmonitor port __builtin_ractor_unmonitor(port) end
#value ⇒ Object
Waits for ractor
to complete, using #join, and return its value or raise the exception which terminated the Ractor
. The value will not be copied even if it is unshareable object. Therefore at most 1 Ractor
can get a value.
r = Ractor.new{ [1, 2] }
r.value #=> [1, 2] (unshareable object)
Ractor.new(r){|r| r.value} #=> Ractor::Error
# File 'ractor.rb', line 573
def value self.join __builtin_ractor_value end