Spec-Zone.ru › Ruby 4.0

класс Ractor

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

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.

Публичные методы класса

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

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

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

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

count () Показать исходный код
# 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
current () Показать исходный код
# 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>
main () Показать исходный код
# File ractor.rb, line 519
def self.main
  __builtin_cexpr! %q{
    rb_ractor_self(GET_VM()->ractor.main_ractor);
  }
end

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

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

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

make_shareable(obj, copy: false) → shareable_obj Показать исходный код
# File ractor.rb, line 459
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, name: nil) {|*args| block } → 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>
receive → obj Показать исходный код
# File ractor.rb, line 349
def self.receive
  Ractor.current.default_port.receive
end

Получает сообщение из порта по умолчанию текущего рактора.

Также имеет псевдоним: recv
recv
Псевдоним для: receive
select(*ractors_or_ports) → [ractor or port, obj] Показать исходный код
# 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
shareable?(obj) → true | false Показать исходный код
# File ractor.rb, line 418
def self.shareable? obj
  __builtin_cexpr! %q{
    RBOOL(rb_ractor_shareable_p(obj));
  }
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.

shareable_lambda(self: nil){} → shareable lambda Показать исходный код
# File ractor.rb, line 681
def self.shareable_lambda self: nil
  Primitive.attr! :use_block

  __builtin_cexpr!(%Q{
    ractor_shareable_proc(ec, *LOCAL_PTR(self), true)
  })
end

То же, что и Ractor.shareable_proc, но возвращает лямбда-функцию Proc.

shareable_proc(self: nil){} → shareable proc Показать исходный код
# File ractor.rb, line 667
def self.shareable_proc self: nil
  Primitive.attr! :use_block

  __builtin_cexpr!(%Q{
    ractor_shareable_proc(ec, *LOCAL_PTR(self), false)
  })
end

Возвращает общую копию 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)
store_if_absent(key){ init_block } Показать исходный код
# 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

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

<<
Псевдоним для: send
[] (sym) Показать исходный код
# 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

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

[]= (sym, val) Показать исходный код
# 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.[]=.

close → true | false Показать исходный код
# File ractor.rb, line 403
def close
  default_port.close
end

Закрывает порт по умолчанию. Закрыть порт может только рактор, который его создал. Поэтому получателем должен быть текущий рактор.

default_port → port object Показать исходный код
# File ractor.rb, line 566
def default_port
  __builtin_cexpr! %q{
    ractor_default_port_value(RACTOR_PTR(self))
  }
end

Возвращает порт по умолчанию для Ractor.

inspect () Показать исходный код
# 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
Также имеет псевдоним: to_s
join → self Показать исходный код
# 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

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

Ractor.new{}.join #=> ractor

Ractor.new{ raise "foo" }.join
#=> raises the exception "foo (RuntimeError)"
monitor(port) → self Показать исходный код
# 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"
    
name () Показать исходный код
# File ractor.rb, line 387
def name
  __builtin_cexpr! %q{RACTOR_PTR(self)->name}
end

Возвращает имя, заданное в Ractor.new, или nil.

recv ()
Псевдоним для: receive
send(msg, move: false) → self Показать исходный код
# File ractor.rb, line 368
def send(...)
  default_port.send(...)
  self
end

Эквивалентно вызову Port#send для default_port рактора.

Также имеет псевдоним: <<
to_s ()
Псевдоним для: inspect
unmonitor(port) → self Показать исходный код
# File ractor.rb, line 644
def unmonitor port
  __builtin_ractor_unmonitor(port)
end

Удаляет порт из списка портов наблюдения этого рактора.

value → obj Показать исходный код
# 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

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

receive () Показать исходный код
# File ractor.rb, line 358
        def receive
  default_port.receive
end

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

Также имеет псевдоним: recv

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