class Ractor

Ractor 是 Ruby 的 Actor 模型抽象,提供线程安全的并行执行。

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(默认实现)上,每个 Ractor 持有全局虚拟机锁 (GVL),因此 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.new 的任何参数都会传递给代码块,并且在那里可以像通过 Ractor.receive 接收一样使用,并且最后一个代码块的值会被发送到 Ractor 外部,就像通过 Ractor.yield 发送一样。

一个经典的 ping-pong 演示

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_incomingRactor#close_outgoing 分别禁用其中任何一个端口。当 Ractor 终止时,其端口会自动关闭。

可共享和不可共享的对象

当对象在 Ractor 之间发送和接收时,了解对象是可共享还是不可共享的非常重要。大多数 Ruby 对象是不可共享的对象。即使是冻结的对象,如果它们包含(通过它们的实例变量)未冻结的对象,也可能是不可共享的。

可共享对象是那些可以被多个线程使用而不会损害线程安全的对象,例如数字、truefalseRactor.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

当可共享对象被发送(通过 sendRactor.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

请注意,数组和数组内未冻结字符串的对象 ID 在 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__)也无法访问移动的对象。

ClassModule 对象是可共享的,因此类/模块定义在 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 是唯一可以访问不可共享常量的 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 上,它们与该 Ractor 的其他线程共享 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

关于代码示例的说明

在下面的示例中,有时我们使用以下方法来等待当前未阻塞的 Ractor 完成(或取得进展)。

def wait
  sleep(0.1)
end

它 **仅用于演示目的**,不应在真实代码中使用。大多数情况下,使用 take 等待 Ractor 完成。

参考

有关更多详细信息,请参阅 Ractor 设计文档