當前位置: 首頁>>編程示例 >>用法及示例精選 >>正文


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類。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。