Spec-Zone.ru › Ruby 3.4

класс Ractor

Родитель:
Объект

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

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).)

Объект необходимо явно разделить:

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 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.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

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

Делимые и неделящиеся объекты

Когда объект отправляется и принимается рактором, важно понимать, является ли он делимым или неделимым. Большинство объектов 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) дополнительная обработка не выполняется. Он просто становится доступным для обоих ракторов. При отправке неделимого объекта, он может быть либо скопирован, либо перемещён. Первое является по умолчанию и полностью копирует объект, выполняя глубокое клонирование (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

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

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

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

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)

Ракторы могут получать доступ к константам, если они делимые. Главный рактор — единственный, который может получить доступ к неделящимся константам.

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 в синтаксисе комментариев.

Ractors против потоков

Каждый рактор имеет свой собственный основной Thread. Новые потоки могут быть созданы внутри ракторов (и, в CRuby, они разделяют GVL с другими потоками этого рактора).

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

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

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

def wait
  sleep(0.1)
end

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

Ссылка

См. документацию по проектированию Ractor для получения более подробной информации.

Методы публичного класса

[] (символ)
Исходный код
# File ractor.rb, line 851
def self.[](sym)
  Primitive.ractor_local_value(sym)
end

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

[]= (символ, значение)
Исходный код
# 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.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

Возвращает главный рактор.

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, если текущий рактор является главным рактором.

make_shareable(объект, копировать: 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 доступным для обмена между ракторами.

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, имя: nil) {|*args| блок } → 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

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

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

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 рактора может быть установлен для целей отладки:

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

Получение сообщения из входящего порта текущего рактора (которое было отправлено туда 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

Если был вызван 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 {|сообщение| блок } → сообщение
Исходный код
# File ractor.rb, line 509
def self.receive_if &b
  Primitive.ractor_receive_if b
end

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

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

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(*ракеты, [значение_выдачи:, перемещать: false]) → [рактор или символ, объект]
Исходный код
# 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

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

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.

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

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 указано, это значение может быть возвращено, если другой рактор вызывает 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?(объект) → true | false
Исходный код
# File ractor.rb, line 785
def self.shareable? obj
  __builtin_cexpr! %q{
    RBOOL(rb_ractor_shareable_p(obj));
  }
end

Проверка, является ли объект разделяемым ракторами.

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

Отправьте сообщение в исходящий порт текущего рактора, чтобы его принял 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 в ракторе и 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 в ракторе и 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.receive.

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

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

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

Попытка отправить сообщение рактору, который уже завершил свою работу, вызовет 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)

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

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.yield или при завершении работы рактора.

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 («просто подождите, пока рактор завершит работу»). Однако, он выбросит ошибку, если кто-то уже получил это сообщение.

Если исходящий порт был закрыт с помощью 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, поэтому завершение рактора прервёт любые циклы, получающие это сообщение, без распространения ошибки:

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
END_OF_DOCUMENT_MARKER

Приватные методы экземпляра

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–2024 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

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