Ractorで複数のオブジェクトを渡せるようにしてみた

結論

Ractorで以下のコードを動くようにしてみました。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# => 1
# => 2
# => Integer
# => Integer

Ractorって?

Ruby3で導入される並行・並列機能を提供するしくみです。
元々はGuildという名前で数年前から議論されてきたものです。

詳しい話は下記の動画を参照して頂ければと思います。

[JA] Ractor report / Koichi Sasada @ko1

Ractorへオブジェクトを渡す

sendメソッドを使ってRactorへとオブジェクトを渡すことができます。

r = Ractor.new do
    v = Ractor.recv
    puts v
    puts v.class
end

r.send(1)

r.take
# => 1
# => Integer

しかし、以下のように複数のオブジェクトを渡すことはできません。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# =>wrong number of arguments (given 2, expected 1) (ArgumentError)

ただし配列で渡す分にはOKみたいです。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send([1, 2])

r.take
# => 1
# => 2
# => Integer
# => Integer

実装を見てみると以下のようになっています(CRubyのソースコード内のractor.rbにて)
現在のsendメソッドは一つのオブジェクトのみを引数に受け取ります。またムーヴして良いかどうかをキーワード引数moveで指定することもできます。

  def send obj, move: false
    __builtin_cexpr! %q{
      ractor_send(ec, RACTOR_PTR(self), obj, move)
    }
  end

__builtin_cexpr!でCの関数を呼び出し、メソッドが受け取った引数をそのままCの関数に渡しています。
余談ですが、最近のCRubyでは内部実装としてRubyの変数をCの関数に渡すようなコードを書くことができるようになっています。

やったこと

Ractorのsendメソッドを以下のように書き替えてみました。

  def send obj, *arg, move: false
    obj = arg.unshift obj unless arg.empty?
    __builtin_cexpr! %q{
      ractor_send(ec, RACTOR_PTR(self), obj, move)
    }
  end

まず、sendメソッドは必ず一つのオブジェクトを引数として受け取っています。その挙動を維持するためにobj, *arg, move: falseのように引数を書き換えています。
またsend(1, 2)のように複数オブジェクトが渡された場合は*argに配列として引数が渡されます。

argが空の配列でない場合、複数オブジェクトが渡されていることになり、最終的にCの関数に渡されるobjを第一引数と可変長引数をマージしたものへと変換しています。

あとは修正したCRubyのソースコードをビルドすればOKです。

これで以下のようにRactorに複数のオブジェクトを渡すことができます。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# => 1
# => 2
# => Integer
# => Integer

参考

ref: Guild → Ractor
ref: https://github.com/ko1/ruby/blob/ractor/ractor.ja.md
ref: [[JA Ractor report / Koichi Sasada @ko1

追記

ちなみに、モンキーパッチでよければ以下のようにラップしたメソッドを作成すればOKです

class Ractor
    def multi_send(obj, *args, move: true)
        obj = args.unshift obj unless args.empty?
        send(obj, move: move)
    end 
end

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.multi_send(1, 2)

r.take

モンキーパッチだと範囲が広いので実務とかで使うならrefinements使った方が影響が少ないのでいいかもしれない。

module RefineRactor
    refine Ractor do
        def multi_send(obj, *args, move: true)
            obj = args.unshift obj unless args.empty?
            send(obj, move: move)
        end 
    end
end

using RefineRactor

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.multi_send(1, 2)

r.take