当前位置: 首页>>代码示例 >>用法及示例精选 >>正文


Ruby Ractor类用法及代码示例


本文简要介绍ruby语言中 Ractor类 的用法。

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

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!" would be printed

Ractors 不共享通常的对象,因此data-race、race-conditions 等相同类型的线程安全问题在multi-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).)

在 CRuby(默认实现)上,全局虚拟机锁 (GVL) 由每个 ractor 持有,因此 ractor 并行执行而不会相互锁定。

与其访问共享状态,不如通过将对象作为消息发送和接收来将对象传入和传出 ractor。

a = 1
r = Ractor.new do
  a_in_ractor = receive # receive blocks till somebody will pass 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" would be printed

有两对发送/接收消息的方法:

除此之外, Ractor.new 的参数将被传递给 block 并在那里可用,就像由 Ractor.receive 接收一样,最后一个块值将被发送到 ractor 之外,就像由 Ractor.yield 发送一样。

经典ping-pong的小演示:

server = Ractor.new 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 inside client, and available as srv
  puts "Client starts: #{self.inspect}"
  received = srv.take                       # The Client takes a message specifically 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 specifically to the server
end

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

这将输出:

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

据说 Ractor 通过 incoming port 接收消息,并将它们发送到 outgoing port 。可以分别使用 Ractor#close_incoming Ractor#close_outgoing 禁用任何一个。如果一个 ractor 终止,它的端口将自动关闭。

可共享和不可共享的对象

当对象被发送到和从 ractor 发送时,了解对象是可共享的还是不可共享的很重要。大多数对象是不可共享的对象。

可共享对象本质上是可以被多个线程使用而不影响线程安全的对象;例如不可变的。 Ractor.shareable? 允许检查这一点,如果不是, Ractor.make_shareable 会尝试使对象可共享。

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

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 使用。发送不可共享对象时,它可以是 copiedmoved 。第一个是默认设置,它通过深度克隆其结构的不可共享部分来制作对象的完整副本。

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内部都发生了变化,表明它是不同的对象。但是第二个数组的元素,即可共享的冻结字符串,具有相同的object_id。)

对象的深度克隆可能很慢,有时甚至是不可能的。或者,move: true 可用于发送。这会将 move 对象发送到接收 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 扩展可以引入它们。

禁止从除 main 之外的 ractor 访问可变可共享对象(尤其是模块和类)的实例变量:

class C
  class << self
    attr_accessor :tricky
  end
end

C.tricky = 'test'

r = Ractor.new(C) do |cls|
  puts "I see #{cls}"
  puts "I can't see #{cls.tricky}"
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'

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)

另请参见Comments syntax 说明中的# shareable_constant_value pragma 说明。

Ractors 与线程

每个 ractor 创建自己的线程。可以从 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 design doc

相关用法


注:本文由纯净天空筛选整理自ruby-lang.org大神的英文原创作品 Ractor类。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。