Spec-Zone.ru › Ruby 3.4

class Ractor

Родительский класс:
Object

Ractor — это абстракция модели акторов для Ruby, обеспечивающая потокобезопасное параллельное выполнение.

Ractor.new создаёт новый Ractor, который может выполняться параллельно.

# The simplest ractor
r = Ractor.new {puts "I am in Ractor!"}
r.take # wait for it to finish
# Here, "I am in Ractor!" is printed

Ractor не разделяют между собой все объекты. Это даёт два основных преимущества: между Ractor невозможны проблемы потокобезопасности, такие как гонки данных и состояния гонки. Второе преимущество — параллелизм.

Для этого совместное использование объектов между Ractor ограничено. Например, в отличие от потоков, Ractor не имеют доступа ко всем объектам, доступным в других Ractor. Даже объекты, обычно доступные через переменные во внешней области видимости, запрещено использовать в разных Ractor.

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) удерживается отдельно для каждого Ractor, поэтому Ractor могут выполняться параллельно, не блокируя друг друга. Это отличается от ситуации с потоками в CRuby.

Вместо доступа к общему состоянию объекты следует передавать в Ractor и из него, отправляя и получая их в виде сообщений.

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.take
# Here, "I am in Ractor! a=1" is printed

Для отправки и получения сообщений предусмотрены две пары методов:

  • Ractor#send и Ractor.receive, когда отправитель знает получателя (push);

  • Ractor.yield и Ractor#take, когда получатель знает отправителя (pull);

Кроме того, все аргументы, переданные в Ractor.new, передаются в блок и доступны в нём так, как если бы они были получены с помощью Ractor.receive, а последнее значение блока отправляется за пределы Ractor так, как если бы оно было отправлено с помощью Ractor.yield.

Небольшая демонстрация классической игры в пинг-понг:

server = Ractor.new(name: "server") do
  puts "Server starts: #{self.inspect}"
  puts "Server sends: ping"
  Ractor.yield 'ping'                       # The server doesn't know the receiver and sends to whoever interested
  received = Ractor.receive                 # The server doesn't know the sender and receives from whoever sent
  puts "Server received: #{received}"
end

client = Ractor.new(server) do |srv|        # The server is sent to the client, and available as srv
  puts "Client starts: #{self.inspect}"
  received = srv.take                       # The client takes a message from the server
  puts "Client received from " \
       "#{srv.inspect}: #{received}"
  puts "Client sends to " \
       "#{srv.inspect}: pong"
  srv.send 'pong'                           # The client sends a message to the server
end

[client, server].each(&:take)               # Wait until they both finish

Будет выведено примерно следующее:

Server starts: #<Ractor:#2 server test.rb:1 running>
Server sends: ping
Client starts: #<Ractor:#3 test.rb:8 running>
Client received from #<Ractor:#2 server test.rb:1 blocking>: ping
Client sends to #<Ractor:#2 server test.rb:1 blocking>: pong
Server received: pong

Ractor получают сообщения через входящий порт и отправляют их через исходящий порт. Каждый из портов можно отключить с помощью Ractor#close_incoming и Ractor#close_outgoing соответственно. При завершении Ractor его порты закрываются автоматически.

Совместно используемые и несовместно используемые объекты

При отправке объекта в Ractor или из него важно понимать, является ли объект совместно используемым или несовместно используемым. Большинство объектов Ruby несовместно используемые. Даже замороженные объекты могут быть несовместно используемыми, если они содержат (через переменные экземпляра) незамороженные объекты.

Совместно используемые объекты — это объекты, которые можно использовать в нескольких потоках без ущерба для потокобезопасности, например числа, 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 или Ractor.yield) дополнительная обработка не выполняется. Объект просто становится доступным для использования обоими Ractor. Несовместно используемый объект при отправке можно либо скопировать, либо переместить. По умолчанию объект копируется целиком посредством глубокого клонирования (Object#clone) несовместно используемых частей его структуры.

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}"

Будет выведено примерно следующее:

In ractor: 340, 360, 320
Outside  : 380, 400, 320

Обратите внимание, что идентификаторы объектов массива и незамороженной строки внутри массива в Ractor изменились, поскольку это другие объекты. Элемент второго массива, представляющий собой совместно используемую замороженную строку, остался тем же объектом.

Глубокое клонирование объектов может быть медленным, а иногда и невозможным. Вместо этого при отправке можно использовать move: true. В этом случае несовместно используемый объект будет перемещён в принимающий Ractor и станет недоступен отправляющему 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}"

Будет выведено:

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 являются совместно используемыми, поэтому определения классов и модулей доступны всем Ractor. Объекты Ractor также являются совместно используемыми. Все операции с совместно используемыми объектами потокобезопасны, поэтому это свойство сохраняется. В Ruby нельзя определять изменяемые совместно используемые объекты, однако их могут создавать расширения на C.

В других Ractor запрещено обращаться (получать) к переменным экземпляра совместно используемых объектов, если значения этих переменных не являются совместно используемыми. Это может произойти, поскольку модули и классы являются совместно используемыми, но могут иметь переменные экземпляра со значениями, которые таковыми не являются. В неосновных Ractor также запрещено устанавливать переменные экземпляра для классов и модулей (даже если значение является совместно используемым).

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)

Ractor могут обращаться к константам, если те являются совместно используемыми. Только основной Ractor может обращаться к несовместно используемым константам.

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)

См. также описание директивы shareable_constant_value в разделе Синтаксис комментариев.

Ractor и потоки

У каждого Ractor есть собственный главный Thread. Внутри Ractor можно создавать новые потоки (в CRuby они совместно используют GVL с другими потоками этого 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

Примечание к примерам кода

В приведённых ниже примерах мы иногда используем следующий метод, чтобы дождаться завершения (или продвижения вперёд) Ractor, которые в данный момент не заблокированы.

def wait
  sleep(0.1)
end

Он предназначен **только для демонстрационных целей** и не должен использоваться в реальном коде. В большинстве случаев для ожидания завершения Ractor используется take.

Справка

Дополнительные сведения см. в документе Проектирование Ractor.

Общедоступные методы класса

[] (sym) Показать исходный код
# File ractor.rb, line 851
def self.[](sym)
  Primitive.ractor_local_value(sym)
end

получает значение из локального хранилища текущего Ractor

[]= (sym, val) Показать исходный код
# File ractor.rb, line 856
def self.[]=(sym, val)
  Primitive.ractor_local_value_set(sym, val)
end

устанавливает значение в локальном хранилище текущего Ractor

count () Показать исходный код
# File ractor.rb, line 302
def self.count
  __builtin_cexpr! %q{
    ULONG2NUM(GET_VM()->ractor.cnt);
  }
end

Возвращает количество Ractor, которые в данный момент выполняются или заблокированы (ожидают).

Ractor.count                   #=> 1
r = Ractor.new(name: 'example') { Ractor.yield(1) }
Ractor.count                   #=> 2 (main + example ractor)
r.take                         # wait for Ractor.yield(1)
r.take                         # wait until r will finish
Ractor.count                   #=> 1
current () Показать исходный код
# File ractor.rb, line 288
def self.current
  __builtin_cexpr! %q{
    rb_ractor_self(rb_ec_ractor_ptr(ec));
  }
end

Возвращает выполняющийся в данный момент Ractor.

Ractor.current #=> #<Ractor:#1 running>
main () Показать исходный код
# File ractor.rb, line 879
def self.main
  __builtin_cexpr! %q{
    rb_ractor_self(GET_VM()->ractor.main_ractor);
  }
end

возвращает основной Ractor

main? () Показать исходный код
# File ractor.rb, line 886
def self.main?
  __builtin_cexpr! %q{
    RBOOL(GET_VM()->ractor.main_ractor == rb_ec_ractor_ptr(ec))
  }
end

возвращает true, если текущий Ractor является основным

make_shareable(obj, copy: false) → shareable_obj Показать исходный код
# File ractor.rb, line 826
def self.make_shareable obj, copy: false
  if copy
    __builtin_cexpr! %q{
      rb_ractor_make_shareable_copy(obj);
    }
  else
    __builtin_cexpr! %q{
      rb_ractor_make_shareable(obj);
    }
  end
end

Делает obj совместно используемым между Ractor.

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.

new(*args, name: nil) {|*args| block } → ractor Показать исходный код
# File ractor.rb, line 273
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)")
    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

Создаёт новый Ractor с аргументами args и блоком.

Переданный блок (Proc) будет изолирован (не сможет обращаться к переменным из внешней области видимости). self внутри блока будет ссылаться на текущий Ractor.

r = Ractor.new { puts "Hi, I am #{self.inspect}" }
r.take
# 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.take
# Prints:
#   Passing: [1, 2, 3] (#280)
#   Received: [1, 2, 3] (#300)

Для отладки можно задать name Ractor:

r = Ractor.new(name: 'my ractor') {}; r.take
p r
#=> #<Ractor:#3 my ractor test.rb:1 terminated>
receive → msg Показать исходный код
# File ractor.rb, line 430
def self.receive
  __builtin_cexpr! %q{
    ractor_receive(ec, rb_ec_ractor_ptr(ec))
  }
end

Получает сообщение из входящего порта текущего Ractor (оно было отправлено туда другим Ractor с помощью send).

r = Ractor.new do
  v1 = Ractor.receive
  puts "Received: #{v1}"
end
r.send('message1')
r.take
# Here will be printed: "Received: message1"

Вместо этого можно использовать закрытый метод экземпляра receive:

r = Ractor.new do
  v1 = receive
  puts "Received: #{v1}"
end
r.send('message1')
r.take
# This prints: "Received: message1"

Если очередь пуста, метод блокируется.

r = Ractor.new do
  puts "Before first receive"
  v1 = Ractor.receive
  puts "Received: #{v1}"
  v2 = Ractor.receive
  puts "Received: #{v2}"
end
wait
puts "Still not received"
r.send('message1')
wait
puts "Still received only one"
r.send('message2')
r.take

Вывод:

Before first receive
Still not received
Received: message1
Still received only one
Received: message2

Если для Ractor был вызван close_incoming, метод вызывает исключение Ractor::ClosedError, если во входящей очереди больше нет сообщений:

Ractor.new do
  close_incoming
  receive
end
wait
# in `receive': The incoming port is already closed => #<Ractor:#2 test.rb:1 running> (Ractor::ClosedError)
Также имеет псевдоним: recv
receive_if {|msg| block } → msg Показать исходный код
# File ractor.rb, line 509
def self.receive_if &b
  Primitive.ractor_receive_if b
end

Получает только определённое сообщение.

В отличие от Ractor.receive, методу Ractor.receive_if можно передать блок с шаблоном (или любым фильтром), чтобы выбрать сообщения из входящей очереди вашего Ractor.

r = Ractor.new do
  p Ractor.receive_if{|msg| msg.match?(/foo/)} #=> "foo3"
  p Ractor.receive_if{|msg| msg.match?(/bar/)} #=> "bar1"
  p Ractor.receive_if{|msg| msg.match?(/baz/)} #=> "baz2"
end
r << "bar1"
r << "baz2"
r << "foo3"
r.take

Будет выведено:

foo3
bar1
baz2

Если блок возвращает истинное значение, сообщение удаляется из входящей очереди и возвращается. В противном случае сообщение остаётся во входящей очереди, а блок применяется к следующим сообщениям.

Если во входящей очереди не осталось сообщений, метод будет заблокирован до поступления новых сообщений.

Если выполнение блока прерывается оператором break/return, исключением или throw, сообщение удаляется из входящей очереди, как если бы было возвращено истинное значение.

r = Ractor.new do
  val = Ractor.receive_if{|msg| msg.is_a?(Array)}
  puts "Received successfully: #{val}"
end

r.send(1)
r.send('test')
wait
puts "2 non-matching sent, nothing received"
r.send([1, 2, 3])
wait

Выводит:

2 non-matching sent, nothing received
Received successfully: [1, 2, 3]

Обратите внимание: нельзя рекурсивно вызывать receive/receive_if из переданного блока. В блоке следует выполнять только фильтрацию сообщений.

Ractor.current << true
Ractor.receive_if{|msg| Ractor.receive}
#=> `receive': can not call receive/receive_if recursively (Ractor::Error)
recv
Псевдоним для: receive
select(*ractors, [yield_value:, move: false]) → [ractor or symbol, obj] Показать исходный код
# File ractor.rb, line 358
def self.select(*ractors, yield_value: yield_unspecified = true, move: false)
  raise ArgumentError, 'specify at least one ractor or `yield_value`' if yield_unspecified && ractors.empty?

  if ractors.delete Ractor.current
    do_receive = true
  else
    do_receive = false
  end

  __builtin_ractor_select_internal ractors, do_receive, !yield_unspecified, yield_value, move
end

Ожидает, пока в исходящем порту какого-либо Ractor появится объект, считывает его из этого Ractor и возвращает Ractor вместе с полученным объектом.

r1 = Ractor.new {Ractor.yield 'from 1'}
r2 = Ractor.new {Ractor.yield 'from 2'}

r, obj = Ractor.select(r1, r2)

puts "received #{obj.inspect} from #{r.inspect}"
# Prints: received "from 1" from #<Ractor:#2 test.rb:1 running>
# But could just as well print "from r2" here, either prints could be first.

Если один из переданных Ractor является текущим Ractor и выбран именно он, r будет содержать символ :receive вместо объекта Ractor.

r1 = Ractor.new(Ractor.current) do |main|
  main.send 'to main'
  Ractor.yield 'from 1'
end
r2 = Ractor.new do
  Ractor.yield 'from 2'
end

r, obj = Ractor.select(r1, r2, Ractor.current)
puts "received #{obj.inspect} from #{r.inspect}"
# Could print: received "to main" from :receive

Если задано yield_value, это значение может быть передано, если другой Ractor вызывает take. В этом случае возвращается пара [:yield, nil]:

r1 = Ractor.new(Ractor.current) do |main|
  puts "Received from main: #{main.take}"
end

puts "Trying to select"
r, obj = Ractor.select(r1, Ractor.current, yield_value: 123)
wait
puts "Received #{obj.inspect} from #{r.inspect}"

Будет выведено:

Trying to select
Received from main: 123
Received nil from :yield

Логический флаг move определяет, будет ли переданное значение скопировано (по умолчанию) или перемещено.

shareable?(obj) → true | false Показать исходный код
# File ractor.rb, line 785
def self.shareable? obj
  __builtin_cexpr! %q{
    RBOOL(rb_ractor_shareable_p(obj));
  }
end

Проверяет, является ли объект совместно используемым для Ractor.

Ractor.shareable?(1)            #=> true -- numbers and other immutable basic values are frozen
Ractor.shareable?('foo')        #=> false, unless the string is frozen due to # frozen_string_literal: true
Ractor.shareable?('foo'.freeze) #=> true

См. также раздел «Совместно используемые и несовместно используемые объекты» в документации класса Ractor.

store_if_absent(key){ init_block } Показать исходный код
# File ractor.rb, line 874
def self.store_if_absent(sym)
  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
yield(msg, move: false) → nil Показать исходный код
# File ractor.rb, line 643
def self.yield(obj, move: false)
  __builtin_cexpr! %q{
    ractor_yield(ec, rb_ec_ractor_ptr(ec), obj, move)
  }
end

Отправляет сообщение в исходящий порт текущего Ractor, откуда его можно получить с помощью take.

r = Ractor.new {Ractor.yield 'Hello from ractor'}
puts r.take
# Prints: "Hello from ractor"

Этот метод блокирующий и возвращает управление только после того, как кто-нибудь получит отправленное сообщение.

r = Ractor.new do
  Ractor.yield 'Hello from ractor'
  puts "Ractor: after yield"
end
wait
puts "Still not taken"
puts r.take

Будет выведено:

Still not taken
Hello from ractor
Ractor: after yield

Если исходящий порт был закрыт с помощью close_outgoing, метод вызовет исключение:

r = Ractor.new do
  close_outgoing
  Ractor.yield 'Hello from ractor'
end
wait
# `yield': The outgoing-port is already closed (Ractor::ClosedError)

Аргумент move имеет то же значение, что и для send.

Общедоступные методы экземпляра

<<
Псевдоним для: send
[] (sym) Показать исходный код
# File ractor.rb, line 840
def [](sym)
  Primitive.ractor_local_value(sym)
end

получает значение из локального хранилища текущего Ractor. Устарел; вместо него используйте Ractor.[].

[]= (sym, val) Показать исходный код
# File ractor.rb, line 846
def []=(sym, val)
  Primitive.ractor_local_value_set(sym, val)
end

устанавливает значение в локальном хранилище текущего Ractor. Устарел; вместо него используйте Ractor.[]=.

close_incoming → true | false Показать исходный код
# File ractor.rb, line 750
def close_incoming
  __builtin_cexpr! %q{
    ractor_close_incoming(ec, RACTOR_PTR(self));
  }
end

Закрывает входящий порт и возвращает значение, указывающее, был ли он уже закрыт. Все последующие попытки вызвать Ractor.receive в Ractor или отправить сообщение в Ractor с помощью send завершатся ошибкой Ractor::ClosedError.

r = Ractor.new {sleep(500)}
r.close_incoming  #=> false
r.close_incoming  #=> true
r.send('test')
# Ractor::ClosedError (The incoming-port is already closed)
close_outgoing → true | false Показать исходный код
# File ractor.rb, line 768
def close_outgoing
  __builtin_cexpr! %q{
    ractor_close_outgoing(ec, RACTOR_PTR(self));
  }
end

Закрывает исходящий порт и возвращает значение, указывающее, был ли он уже закрыт. Все последующие попытки вызвать Ractor.yield в Ractor или получить сообщение из Ractor с помощью take завершатся ошибкой Ractor::ClosedError.

r = Ractor.new {sleep(500)}
r.close_outgoing  #=> false
r.close_outgoing  #=> true
r.take
# Ractor::ClosedError (The outgoing-port is already closed)
inspect () Показать исходный код
# File ractor.rb, line 716
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
Также имеет псевдоним: to_s
name () Показать исходный код
# File ractor.rb, line 729
def name
  __builtin_cexpr! %q{RACTOR_PTR(self)->name}
end

Имя, заданное в Ractor.new, или nil.

recv ()
Псевдоним для: receive
send(msg, move: false) → self Показать исходный код
# File ractor.rb, line 599
def send(obj, move: false)
  __builtin_cexpr! %q{
    ractor_send(ec, RACTOR_PTR(self), obj, move)
  }
end

Отправляет сообщение во входящую очередь Ractor, откуда его можно получить с помощью Ractor.receive.

r = Ractor.new do
  value = Ractor.receive
  puts "Received #{value}"
end
r.send 'message'
# Prints: "Received: message"

Метод неблокирующий (возвращает управление сразу, даже если Ractor не готов что-либо принять):

r = Ractor.new {sleep(5)}
r.send('test')
puts "Sent successfully"
# Prints: "Sent successfully" immediately

Попытка отправить сообщение в Ractor, выполнение которого уже завершилось, вызовет исключение Ractor::ClosedError.

r = Ractor.new {}
r.take
p r
# "#<Ractor:#6 (irb):23 terminated>"
r.send('test')
# Ractor::ClosedError (The incoming-port is already closed)

Если для Ractor был вызван close_incoming, метод также вызывает исключение Ractor::ClosedError.

r =  Ractor.new do
  sleep(500)
  receive
end
r.close_incoming
r.send('test')
# Ractor::ClosedError (The incoming-port is already closed)
# The error is raised immediately, not when the ractor tries to receive

Если obj является несовместно используемым, по умолчанию он будет скопирован в принимающий Ractor посредством глубокого клонирования. Если передан move: true, объект будет перемещён в принимающий Ractor и станет недоступен отправителю.

r = Ractor.new {puts "Received: #{receive}"}
msg = 'message'
r.send(msg, move: true)
r.take
p msg

Будет выведено:

Received: message
in `p': undefined method `inspect' for #<Ractor::MovedObject:0x000055c99b9b69b8>

Все ссылки на объект и его части станут недействительными для отправителя.

r = Ractor.new {puts "Received: #{receive}"}
s = 'message'
ary = [s]
copy = ary.dup
r.send(ary, move: true)

s.inspect
# Ractor::MovedError (can not send any methods to a moved object)
ary.class
# Ractor::MovedError (can not send any methods to a moved object)
copy.class
# => Array, it is different object
copy[0].inspect
# Ractor::MovedError (can not send any methods to a moved object)
# ...but its item was still a reference to `s`, which was moved

Если объект является совместно используемым, move: true не повлияет на него:

r = Ractor.new {puts "Received: #{receive}"}
s = 'message'.freeze
r.send(s, move: true)
s.inspect #=> "message", still available
Также имеет псевдоним: <<
take → msg Показать исходный код
# File ractor.rb, line 710
def take
  __builtin_cexpr! %q{
    ractor_take(ec, RACTOR_PTR(self))
  }
end

Получает сообщение из исходящего порта Ractor, куда оно было помещено с помощью Ractor.yield или при завершении Ractor.

r = Ractor.new do
  Ractor.yield 'explicit yield'
  'last value'
end
puts r.take #=> 'explicit yield'
puts r.take #=> 'last value'
puts r.take # Ractor::ClosedError (The outgoing-port is already closed)

Поскольку последнее значение также отправляется в исходящий порт, take можно использовать как аналог Thread#join («просто подождать завершения Ractor»). Однако метод вызовет исключение, если это сообщение уже было получено кем-то другим.

Если исходящий порт был закрыт с помощью close_outgoing, метод вызовет исключение Ractor::ClosedError.

r = Ractor.new do
  sleep(500)
  Ractor.yield 'Hello from ractor'
end
r.close_outgoing
r.take
# Ractor::ClosedError (The outgoing-port is already closed)
# The error would be raised immediately, not when ractor will try to receive

Если в Ractor возникает неперехваченное исключение, оно передаётся методом take как Ractor::RemoteError.

r = Ractor.new {raise "Something weird happened"}

begin
  r.take
rescue => e
  p e              #  => #<Ractor::RemoteError: thrown by remote Ractor.>
  p e.ractor == r  # => true
  p e.cause        # => #<RuntimeError: Something weird happened>
end

Ractor::ClosedError является потомком StopIteration, поэтому завершение Ractor прерывает циклы, получающие это сообщение, не передавая исключение дальше:

r = Ractor.new do
  3.times {|i| Ractor.yield "message #{i}"}
  "finishing"
end

loop {puts "Received: " + r.take}
puts "Continue successfully"

Будет выведено:

Received: message 0
Received: message 1
Received: message 2
Received: finishing
Continue successfully
to_s ()
Псевдоним для: inspect

Закрытые методы экземпляра

receive () Показать исходный код
# File ractor.rb, line 441
        def receive
  __builtin_cexpr! %q{
    ractor_receive(ec, rb_ec_ractor_ptr(ec))
  }
end

то же, что и Ractor.receive

Также имеет псевдоним: recv
receive_if (&b) Показать исходный код
# File ractor.rb, line 514
        def receive_if &b
  Primitive.ractor_receive_if b
end

то же, что и Ractor.receive_if

Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API