【Ruby 3.0 Advent Calendar 2020】Ractor の共有可能オブジェクトについて【24日目】

Ruby 3.0 Advent Calendar 2020 24日目の記事になります。

今回は Ruby 3.0 で実験的に入る Ractor の共有可能オブジェクトについて簡単に説明してみます。

NOTE: ここに書いてある内容は今後変更されるかもしれないので注意してください!!

共有可能オブジェクトとは

通常 Ractor 間でオブジェクトのやり取りを場合は #send + Ractor.receiveRactor.yield + #take をつかったりします。

ractor = Ractor.new {
  # send の値を受け取る
  obj = Ractor.receive
  p obj
  # => [1, 2, 3]

  # take の戻り値として返す
  Ractor.yield obj.map { _1 + _1 }
}

# send で渡した値を Ractor.receive で受け取る
ractor.send [1, 2, 3]

# Ractor.yield の引数を受け取る
p ractor.take

こんな感じで任意のオブジェクトを渡したり受け取ったりします。
この時に普通にオブジェクトを渡した場合はそのコピーが Ractor へと渡されます。

ractor = Ractor.new {
  obj = Ractor.receive
  p obj.__id__
  # => 80
}

obj = [1, 2, 3]
p obj.__id__
# => 60

# obj のコピーを Ractor へと渡す
ractor.send obj

ractor.take

この時に objfreeze するとコピーされずに Ractor へと渡されます。

ractor = Ractor.new {
  obj = Ractor.receive
  p obj.__id__
  # => 60
}

obj = [1, 2, 3]
obj.freeze
p obj.__id__
# => 60

# obj はコピーされずに Ractor へと渡される
ractor.send obj

ractor.take

このように『コピーせずに渡されるオブジェクト』の事を『共有可能オブジェクト』と呼びます。
『共有可能オブジェクト』は Ractor.shareable? で判定する事ができます。
共有可能オブジェクトの条件は以下になります。

  • 不変なオブジェクト
    • freeze されているオブジェクト
    • 整数やシンボルなどはデフォルトで freeze されているのでこれに該当する
    • オブジェクトが参照する要素が全て不変である必要がある
  • クラス・モジュール
  • その他、特別なオブジェクト
    • Ractor オブジェクトなどなど
# 共有可能オブジェクト
p Ractor.shareable? 1               # => true
p Ractor.shareable? :hoge           # => true
p Ractor.shareable? nil             # => true
p Ractor.shareable? false           # => true
p Ractor.shareable? [1, 2].freeze   # => true
p Ractor.shareable? "hoge".freeze   # => true
p Ractor.shareable? Array           # => true
class X; end
p Ractor.shareable? X               # => true
p Ractor.shareable? Ractor.new {}   # => true
puts "-------------"

# 共有可能オブジェクトではない
p Ractor.shareable? [1, 2]                  # => false
p Ractor.shareable? "hoge"                  # => false
p Ractor.shareable? [1, 2, "hoge"]          # => false
p Ractor.shareable? [1, 2, "hoge"].freeze   # => false

共有可能オブジェクト化する

共有可能オブジェクト化する手段はいくつかあるので紹介します。

.freeze する

一番シンプルなのが .freeze する方法です。

obj = "hoge"

# 共有可能オブジェクトではない
p Ractor.shareable? obj   # => false

# freeze すると共有可能オブジェクトとして扱われる
obj.freeze
p Ractor.shareable? obj   # => true

ただし、配列やハッシュなどは保持している要素全てが .freeze されている必要があります。

obj = [1, 2, "hoge"]

# 共有可能オブジェクトではない
p Ractor.shareable? obj   # => false

# obj だけ freeze してもダメ
obj.freeze
p Ractor.shareable? obj   # => false

# 要素も全て freeze されている必要がある
obj[2].freeze
p Ractor.shareable? obj   # => true

Ractor.make_shareable

先程のネストした配列のようなオブジェクトを一発で共有可能オブジェクトにしたい場合には Ractor.make_shareable が利用できます。

obj = [1, 2, "hoge"]

# 共有可能オブジェクトではない
p Ractor.shareable? obj   # => false

# obj の中身を全て freeze する
Ractor.make_shareable obj
p Ractor.shareable? obj   # => true
p obj.frozen?             # => true
p obj[2].frozen?          # => true

基本的に任意のオブジェクトを共有可能オブジェクトにしたい場合は Ractor.make_shareable を利用するといいと思います。

マジックコメントで定数を共有可能オブジェクト化する

専用のマジックコメントを記述しておくことで定数をデフォルトで共有可能オブジェクトとして定義する事ができます。

  • experimental_everything : マジックコメント移行の定数定義を共有可能オブジェクトにする
  • experimental_copy : 値をコピーを共有可能オブジェクトにして定数を定義する
  • none : shareable_constant_value を無効にする
  • literal : 定数定義がリテラルだった場合のみ共有可能オブジェクトにすり

experimental_everything

# マジックコメント以下の定数が共有可能オブジェクトとして定義される
# shareable_constant_value: experimental_everything

# デフォルトで定数が共有可能オブジェクトになる
A = [1, 2, 3]
p Ractor.shareable? A    # => true

# この場合は obj も共有可能オブジェクトになる
obj = [1, 2, 3]
B = obj
p Ractor.shareable? B    # => true
p Ractor.shareable? obj  # => true
# id も同じ
p obj.__id__   # => 60
p B.__id__     # => 60

experimental_copy

# マジックコメント以下の定数が共有可能オブジェクトとして定義される
# 値をコピーしてから定数を定義する
# shareable_constant_value: experimental_copy

# デフォルトで定数が共有可能オブジェクトになる
A = [1, 2, 3]
p Ractor.shareable? A    # => true

# experimental_copy の場合は obj は共有可能オブジェクトにはならない
obj = [1, 2, 3]
B = obj
p Ractor.shareable? B    # => true
p Ractor.shareable? obj  # => false
# id も異なる
p obj.__id__     # => 60
p B.__id__       # => 80

none

# shareable_constant_value: experimental_everything

A = [1, 2, 3]
p Ractor.shareable? A    # => true


# shareable_constant_value の設定を無効にする
# shareable_constant_value: none

B = [1, 2, 3]
p Ractor.shareable? B    # => false

literal

# shareable_constant_value: literal

# これは OK
A = [1, 2, 3]
p Ractor.shareable? A    # => true

# 式を渡した場合にエラーになる
# error: `ensure_shareable': cannot assign unshareable object to B (Ractor::IsolationError)
B = [1, 2, 3] + [4, 5, 6]

まとめ

と、言う感じで現時点でわたしが把握している情報をまとめてみました。
冒頭にも書きましたが Ractor はまだ開発中なので今後ここに書かれている挙動は変わるかもしれません。
Ractor を実際に使用する場合はオフィシャルなドキュメント等も合わせて参照してください。

参照