класс Ractor
Ractor.new создаёт новый Ractor, который может выполняться параллельно с другими ракторами.
# 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
Ракторы не разделяют между собой все объекты. Это даёт два основных преимущества: между ракторaми невозможны связанные с безопасностью потоков проблемы, такие как гонки данных и состояния гонки. Другое преимущество — параллелизм.
Для этого обмен объектами между ракторами ограничен. В отличие от потоков, ракторам недоступны все объекты, доступные в других ракторaх. Например, запрещено использовать в разных ракторaх объекты, обычно доступные через переменные во внешней области видимости.
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).)
Объект необходимо явно сделать общим:
a = 1
r = Ractor.new(a) { |a1| puts "I am in Ractor! a=#{a1}"}
В CRuby (реализации по умолчанию) глобальная блокировка виртуальной машины (GVL) удерживается для каждого рактора, поэтому рактора могут выполняться параллельно. Это отличается от ситуации с потоками в CRuby.
Вместо доступа к общему состоянию объекты следует передавать между ракторами, отправляя и получая их в виде сообщений.
a = 1
r = Ractor.new do
a_in_ractor = receive # receive blocks the Thread until our default port gets sent 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
Кроме того, любые аргументы, переданные в Ractor.new, передаются блоку и доступны в нём так, как если бы они были получены с помощью Ractor.receive, а последнее значение блока можно получить с помощью Ractor#value.
Общие и необщие объекты
При отправке объекта рактору важно понимать, является ли он общим или необщим. Большинство объектов Ruby являются необщими. Даже замороженные объекты могут быть необщими, если они содержат (через переменные экземпляра) незамороженные объекты.
Общие объекты можно одновременно использовать в нескольких ракторaх без ущерба для безопасности потоков, например числа, true и false. Метод Ractor.shareable? позволяет это проверить, а Ractor.make_shareable пытается сделать объект общим, если он ещё не является таковым, и выдаёт ошибку, если это невозможно.
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
При отправке общего объекта с помощью send дополнительная обработка не выполняется, и объект становится доступен обоим ракторaм. При отправке необщего объекта его можно либо скопировать, либо переместить. По умолчанию объект копируется полностью путём глубокого клонирования (Object#clone) необщих частей его структуры.
data = ['foo'.dup, '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.join
puts "Outside : #{data.object_id}, #{data[0].object_id}, #{data[1].object_id}"
Будет выведено примерно следующее:
In ractor: 8, 16, 24 Outside : 32, 40, 24
Обратите внимание, что идентификаторы объектов массива и незамороженной строки внутри массива в ракторе изменились, поскольку это другие объекты. Второй элемент массива — общая замороженная строка — остался тем же объектом.
Глубокое клонирование объектов может быть медленным, а иногда и невозможным. Вместо этого при отправке можно использовать move: true. Это переместит необщий объект в принимающий рактор, сделав его недоступным отправляющему рактору.
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.join
puts "Outside: moved? #{Ractor::MovedObject === data}"
puts "Outside: #{data.inspect}"
Будет выведено:
In ractor: 100, 120 Outside: moved? true test.rb:9:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)
Обратите внимание, что даже inspect и более простые методы, такие как __id__, недоступны для перемещённого объекта.
Объекты Class и Module являются общими, а определения их классов и модулей разделяются между ракторaми. Объекты Ractor также являются общими. Все операции с общими объектами безопасны для потоков во всех ракторaх. В Ruby невозможно определять изменяемые общие объекты, но их могут создавать расширения на C.
Запрещено обращаться (получать) к переменным экземпляра общих объектов из других ракторов, если значения этих переменных не являются общими. Это может произойти, поскольку модули и классы являются общими, но у них могут быть переменные экземпляра со значениями, которые не являются общими. В неродительских ракторaх также запрещено устанавливать переменные экземпляра классов и модулей (даже если значение является общим).
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.join
# I see C
# can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
Ракторы могут обращаться к константам, если они являются общими. Только главный Ractor может обращаться к необщим константам.
GOOD = 'good'.freeze
BAD = 'bad'.dup
r = Ractor.new do
puts "GOOD=#{GOOD}"
puts "BAD=#{BAD}"
end
r.join
# 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.join
# I see C
# can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
См. также описание директивы # shareable_constant_value в разделе Синтаксис комментариев.
Ракторы и потоки
У каждого рактора есть собственный главный Thread. Потоки можно создавать внутри рактора (и в CRuby они используют GVL совместно с другими потоками этого рактора).
r = Ractor.new do
a = 1
Thread.new {puts "Thread in ractor: a=#{a}"}.join
end
r.join
# Here "Thread in ractor: a=1" will be printed
Примечание к примерам кода
В приведённых ниже примерах иногда используется следующий метод, чтобы дождаться выполнения ракторов или завершения их работы.
def wait sleep(0.1) end
Это предназначено **только для демонстрационных целей** и не должно использоваться в реальном коде. В большинстве случаев для ожидания завершения работы ракторов используется join, а для ожидания сообщений — Ractor.receive.
Справка
Подробнее см. документ о проектировании Ractor.
Публичные методы класса
# File ractor.rb, line 490 def self.[](sym) Primitive.ractor_local_value(sym) end
Получает значение из локального хранилища текущего Ractor.
# File ractor.rb, line 495 def self.[]=(sym, val) Primitive.ractor_local_value_set(sym, val) end
Устанавливает значение в локальном хранилище текущего Ractor.
# File ractor.rb, line 258
def self.count
__builtin_cexpr! %q{
ULONG2NUM(GET_VM()->ractor.cnt);
}
end Возвращает количество ракторов, которые в данный момент выполняются или заблокированы (ожидают).
Ractor.count #=> 1
r = Ractor.new(name: 'example') { Ractor.receive }
Ractor.count #=> 2 (main + example ractor)
r << 42 # r's Ractor.receive will resume
r.join # wait for r's termination
Ractor.count #=> 1
# File ractor.rb, line 244
def self.current
__builtin_cexpr! %q{
rb_ractor_self(rb_ec_ractor_ptr(ec));
}
end Возвращает выполняющийся в данный момент Ractor.
Ractor.current #=> #<Ractor:#1 running>
# File ractor.rb, line 519
def self.main
__builtin_cexpr! %q{
rb_ractor_self(GET_VM()->ractor.main_ractor);
}
end Возвращает главный рактор.
# File ractor.rb, line 526
def self.main?
__builtin_cexpr! %q{
RBOOL(GET_VM()->ractor.main_ractor == rb_ec_ractor_ptr(ec))
}
end Возвращает true, если текущий рактор является главным.
Делает obj общим для ракторов.
obj и все объекты, на которые он ссылается, будут заморожены, если они ещё не являются общими.
Если ключевое слово copy имеет значение true, объекты будут скопированы перед замораживанием, а obj и его внутренние объекты не будут изменены.
Обратите внимание, что спецификация и реализация этого метода пока не окончательны и могут измениться в будущем.
obj = ['test'] Ractor.shareable?(obj) #=> false Ractor.make_shareable(obj) #=> ["test"] Ractor.shareable?(obj) #=> true obj.frozen? #=> true obj[0].frozen? #=> true # Copy vs non-copy versions: obj1 = ['test'] obj1s = Ractor.make_shareable(obj1) obj1.frozen? #=> true obj1s.object_id == obj1.object_id #=> true obj2 = ['test'] obj2s = Ractor.make_shareable(obj2, copy: true) obj2.frozen? #=> false obj2s.frozen? #=> true obj2s.object_id == obj2.object_id #=> false obj2s[0].object_id == obj2[0].object_id #=> false
См. также раздел «Общие и необщие объекты» в документации класса Ractor.
# File ractor.rb, line 229
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 API is experimental and may change in future versions of Ruby.",
uplevel: 0, category: :experimental)
end
loc = caller_locations(1, 1).first
loc = "#{loc.path}:#{loc.lineno}"
__builtin_ractor_create(loc, name, args, b)
end Создаёт новый Ractor с аргументами args и блоком.
Переданный блок (Proc) изолирован (не может обращаться к переменным из внешней области видимости). Внутри блока self будет ссылаться на текущий Ractor.
r = Ractor.new { puts "Hi, I am #{self.inspect}" }
r.join
# Prints "Hi, I am #<Ractor:#2 test.rb:1 running>"
Все переданные args передаются аргументам блока по тем же правилам, что и объекты, отправленные с помощью send/Ractor.receive. Если аргумент в args не является общим, он будет скопирован (путём глубокого клонирования, которое может быть неэффективным).
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.join
# Prints:
# Passing: [1, 2, 3] (#280)
# Received: [1, 2, 3] (#300)
Имя рактора name можно задать для целей отладки:
r = Ractor.new(name: 'my ractor') {}; r.join
p r
#=> #<Ractor:#3 my ractor test.rb:1 terminated>
# File ractor.rb, line 349 def self.receive Ractor.current.default_port.receive end
Получает сообщение из порта по умолчанию текущего рактора.
# File ractor.rb, line 308
def self.select(*ports)
raise ArgumentError, 'specify at least one Ractor::Port or Ractor' 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 Блокирует текущий Thread, пока один из указанных портов не получит сообщение. Возвращает массив из двух элементов: первый элемент — это Port, а второй — полученный объект. Этот метод также принимает сами объекты Ractor; в этом случае он ожидает завершения одного из них и возвращает массив из двух элементов, где первый элемент — рактор, а второй — значение, возвращённое при его завершении.
p1, p2 = Ractor::Port.new, Ractor::Port.new
ps = [p1, p2]
rs = 2.times.map do |i|
Ractor.new(ps.shift, i) do |p, i|
sleep rand(0.99)
p.send("r#{i}")
sleep rand(0.99)
"r#{i} done"
end
end
waiting_on = [p1, p2, *rs]
until waiting_on.empty?
received_on, obj = Ractor.select(*waiting_on)
waiting_on.delete(received_on)
puts obj
end
# r0
# r1
# r1 done
# r0 done
Следующий пример почти эквивалентен ractors.map(&:value), за исключением того, что поток разблокируется при завершении любого из ракторов, а не ожидает их завершения в порядке элементов массива.
values = [] until ractors.empty? r, val = Ractor.select(*ractors) ractors.delete(r) values << val end
Проверяет, является ли объект общим для ракторов.
Ractor.shareable?(1) #=> true -- numbers are shareable
Ractor.shareable?('foo') #=> false, unless the string is frozen due to # frozen_string_literal: true
Ractor.shareable?('foo'.freeze) #=> true
См. также раздел «Общие и необщие объекты» в документации класса Ractor.
То же, что и Ractor.shareable_proc, но возвращает лямбда-функцию Proc.
Возвращает общую копию Proc переданного блока. Значение self в Proc будет заменено значением, переданным через ключевое слово ‘self:`, или nil, если оно не указано.
В общей Proc запрещён доступ к переменным из внешней области видимости.
a = 42
Ractor.shareable_proc{ p a }
#=> can not isolate a Proc because it accesses outer variables (a). (ArgumentError)
Значение ‘self` в Proc должно быть общим объектом.
Ractor.shareable_proc(self: self){}
#=> self should be shareable: main (Ractor::IsolationError)
# File ractor.rb, line 513 def self.store_if_absent(sym) Primitive.attr! :use_block Primitive.ractor_local_value_store_if_absent(sym) end
Если соответствующее локальное значение рактора не задано, вычисляет значение с помощью init_block и сохраняет его потокобезопасным способом. Метод возвращает сохранённое значение.
(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 473
def [](sym)
if (self != Ractor.current)
raise RuntimeError, "Cannot get ractor local storage for non-current ractor"
end
Primitive.ractor_local_value(sym)
end # File ractor.rb, line 482
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 Устанавливает значение в локальном хранилище текущего Ractor. Устарел; вместо него используйте Ractor.[]=.
# File ractor.rb, line 403 def close default_port.close end
Закрывает порт по умолчанию. Закрыть порт может только рактор, который его создал. Поэтому получателем должен быть текущий рактор.
# File ractor.rb, line 566
def default_port
__builtin_cexpr! %q{
ractor_default_port_value(RACTOR_PTR(self))
}
end Возвращает порт по умолчанию для Ractor.
# File ractor.rb, line 374
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 # File ractor.rb, line 585
def join
port = Port.new
self.monitor port
if port.receive == :aborted
__builtin_ractor_value
end
self
ensure
port.close
end # File ractor.rb, line 634 def monitor port __builtin_ractor_monitor(port) end
Регистрирует порт как порт наблюдения для этого рактора. При завершении работы рактора порт получает объект Symbol.
-
:exitedотправляется, если рактор завершается без необработанного исключения. -
:abortedотправляется, если рактор завершается из-за необработанного исключения.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 #=> :aborted and r is terminated by the RuntimeError "foo"
# File ractor.rb, line 387
def name
__builtin_cexpr! %q{RACTOR_PTR(self)->name}
end Возвращает имя, заданное в Ractor.new, или nil.
# File ractor.rb, line 368 def send(...) default_port.send(...) self end
Эквивалентно вызову Port#send для default_port рактора.
# File ractor.rb, line 644 def unmonitor port __builtin_ractor_unmonitor(port) end
Удаляет порт из списка портов наблюдения этого рактора.
# File ractor.rb, line 611 def value self.join __builtin_ractor_value end
Ожидает завершения ractor и возвращает его значение либо вызывает исключение, из-за которого завершился Ractor. Значение завершения будет перемещено в вызывающий Ractor. Поэтому получить значение завершения другого рактора может не более одного Ractor.
r = Ractor.new{ [1, 2] }
r.value #=> [1, 2] (unshareable object)
Ractor.new(r){|r| r.value} #=> Ractor::Error
Приватные методы экземпляра
# File ractor.rb, line 358
def receive
default_port.receive
end то же, что и Ractor.receive
Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.