Zenn
Open6

Ractor プログラミングメモ

osyoyuosyoyu

標準入出力へのアクセス

Ruby では特殊変数 $stdin $stdout $stderr および定数 Object::STDIN Object::STDOUT Object::STDERR を通して標準入出力にアクセスできる。
Ractor 内で使えるのは前者だけ。後者は Ractor::IsolationError を発生させる。

r = Ractor.new do
  # this is ok
  $stdout.puts "Hello via $stdout"

  # can not access non-shareable objects in constant Object::STDOUT by non-main Ractor. (Ractor::IsolationError)
  STDOUT.puts "Hello via STDOUT"

  Ractor.yield 1
end
r.take
osyoyuosyoyu

Logger

標準の Logger は shareable ではなく、Ractor の外側で作成したインスタンスを Ractor 内で使うことができない。

Ractorはshareableであるため、ロガーとして振る舞うRactorを作成し、それを他のRactorにわたすとよい。ロガーとしての使い勝手はもちろん Logger より劣る。

logger = Ractor.new do
  require 'logger'
  l = Logger.new($stdout)
  loop do
    log = Ractor.receive
    break if log == :close
    l.info(log) # debug や warn などの呼び分けはできない
  end
  Ractor.yield 1
end

r1 = Ractor.new(logger) do |logger|
  logger.send("Hello from r1")
  Ractor.yield 1
end

r2 = Ractor.new(logger) do |logger|
  logger.send("Hello from r2")
  Ractor.yield 1
end

# join r1 and r2
r1.take
r2.take
logger.send(:close)
logger.take
osyoyuosyoyu

Ractor.new { } の範囲を小さく留める

勢いよくRack server全体をRactorにしまいたい気持ちは分かるが、Ractor-safe / Ractor-shareable でない要素はものすごく多い。

CPU boundな処理を見極めて、そこだけを別Ractorに動かすのでも十分に意味がある…… と思いたい。

rc = Ractor.new(str) { |str| Ractor.yield JSON.parse(str) }
parsed = rc.take
osyoyuosyoyu

proc は Ractor から持ち出すこともできない

proc はかなり色々なところに潜んでいる。たとえば Hash の初期化用 proc を指定している場合、その Hash は yield できなくなる。

Ractor.new do
  hash = Hash.new { |h, k| h[k] = [] }
  # ...
  Ractor.yield hash # <internal:ractor>:644:in 'Ractor.yield': allocator undefined for Proc (TypeError)
end

この例に限っては hash.default = nil とすることで yield 可能になる。

osyoyuosyoyu

Ractor 間で共有したい状態は Ractor にする

ミュータブルな変数は Ractor shareable ではないが、Ractor にくるむことで shareable にできる。

counter の例:

counter_ractor = Ractor.new do
  count = 0
  while msg = Ractor.receive
    case msg
    in [:incr, n]
      count += n
    in [:current, receiver]
      receiver.send(count) # 受信した Ractor のチャンネルを通して結果を送り返す
    end
  end
end

r1 = Ractor.new(counter_ractor) do |counter|
  loop do
    counter.send([:incr, 1])
  end
end

r2 = Ractor.new(counter_ractor) do |counter|
  loop do
    counter.send([:current, Ractor.current]) # 自身の Ractor のチャンネルを通して結果を受信する
    p Ractor.receive
    sleep 1 
  end
end

r1.take; r2.take # join
osyoyuosyoyu

can not isolate a Proc because it accesses outer variables

外の変数にアクセスするとダメ

ary = [1, 2, 3]
Ractor.new do
  p ary
end

<internal:ractor>:282:in 'Ractor.new': can not isolate a Proc because it accesses outer variables (ary). (ArgumentError)
        from err.rb:2:in '<main>'

外にある変数名を使っているとダメ

conn = server.accept
rc = Ractor.new do
  conn = Ractor.receive # うっかり外にある変数名と同じものを使ってしまうとダメ
  # ...
end
rc.send(conn, move: true)
作成者以外のコメントは許可されていません