Ruby: はじめてRactorを触ってみた学習メモ(翻訳)
本記事は私の学習メモです。Ractorを使うと以下の警告が英文で表示されることをお忘れなく。
警告: Ractorは実験的機能であり、今後のRubyバージョンで変更される可能性があります。また、実装上の問題も多数あります。
警告を終えたので、それでは始めましょう。
🔗 アクターを作成する
シンプルなRactorは以下のように作れます。
simple = Ractor.new do
# 何かする
end
アクターのイニシャライザを使うと、作成されるアクターに引数を渡せるようになります。このときにアクターの名前も設定できます。
Ractor.new()
に渡される引数は、指定されたブロックのブロックパラメータになります。ただし、インタープリタはパラメータオブジェクトへの参照を渡すのではなく、メッセージとして送信します。
obj = Object.new
val = "I'm a value"
simple = Ractor.new(obj, val, name: "Simple actor") do |obj, val|
"#{name} has: obj: #{obj.object_id}, val: #{val.object_id}"
end
puts "Created here: obj: #{obj.object_id}, val: #{val.object_id}"
puts simple.take
上の実行結果は以下のようになります。
Created here: obj: 167328, val: 167336
Simple actor has: obj: 167344, val: 167352
引数に渡した数値は、作成された数値と明らかに異なっています(参考)。
もう1つ知っておくべきことがあります。
> simple
=> #<Ractor:#5 Simple actor (irb):57 terminated>
このシンプルなアクターは、結果を生成した後で終了しています(terminated
)。終了したアクターはもうメッセージを受け取らないので、以下を実行するとエラーになります。
> simple.send "anything"
<internal:ractor>:600:in 'Ractor#send': The incoming-port is already closed (Ractor::ClosedError)
また、結果は出力ボックス(outbox)から既に取得済みなので、同じ結果を再度取り出すことはできません。
> simple.take
<internal:ractor>:711:in 'Ractor#take': The outgoing-port is already closed (Ractor::ClosedError)
アクターは、「メッセージを繰り返し生成するもの」と定義できます。
以下のup_to_3_times
に対してtake
を実行するたびに、yield
が1個ずつ「消費」されます。
up_to_3_times = Ractor.new do
Ractor.yield 3
Ractor.yield 2
Ractor.yield 1
end
ここで興味深いのは、up_to_3_times
アクターは値を4つ生成することです。
> up_to_3_times.take
=> 3
> up_to_3_times.take
=> 2
> up_to_3_times.take
=> 1
> up_to_3_times.take
=> nil
> up_to_3_times.take
<internal:ractor>:711:in 'Ractor#take': The outgoing-port is already closed (Ractor::ClosedError)
最初の3
、2
、1
は予想通りの結果です。
しかし4つ目のtake
ではnil
値が返されます(ブロックの戻り値もアクターによってyield
されるため)。
今度はアクターを以下のように更新します。
up_to_3_times = Ractor.new do
Ractor.yield 3
Ractor.yield 2
Ractor.yield 1
"finished here"
end
この場合の結果は以下のようになります。
> loop { puts up_to_3_times.take }
3
2
1
finished here
=> nil
これを避けるには、以下のようにclose_outgoing
メソッドを使えます。
up_to_3_times = Ractor.new do
Ractor.yield 3
Ractor.yield 2
Ractor.yield 1
close_outgoing
end
上の結果は以下のようになります。
> loop { puts up_to_3_times.take }
3
2
1
=> nil
上のサンプルでは既にアクターの通信メカニズムが使われていますが、今度はそれを詳しく調べてメッセージを送受信してみましょう。
🔗 メッセージを送受信する
アクターはメッセージを送受信する形で他の部分とやりとりします。以下は、テキスト文字列を送信して大文字で表示するアクターの例です。
upcase = Ractor.new do
msg = Ractor.receive
Ractor.yield msg.upcase
end
このアクターはメッセージを待ち受けます。
これにメッセージを送信するには、以下のようにsend
を使います。
> upcase.send "abc"
=> #<Ractor:#14 (irb):123 running>
アクターはメッセージを受信して処理し、結果を出力ボックスに送信します。結果を調べるには、そのアクターの出力ボックスからtake
で結果を取り出す必要があります。
> upcase.take
=> "ABC"
このアクターは短命であることを覚えておいてください。
次はアクターをもう少し長生きさせてみましょう。
🔗 長時間実行されるアクター
アクターにもっと長生きしてもらって複数のメッセージを処理するための実装は、以下のような感じになります。
upcase = Ractor.new do
loop do
Ractor.yield receive.upcase
end
end
これで、渡した文字列をすべて大文字にする処理を何度も実行できるようになります。
upcase.send "aaa"
upcase.send "bbb"
upcase.send "ccc"
アクターにメッセージを送信すると、メッセージは受信ボックス(inbox)に到達します。
receive
メソッドは、受信ボックスからメッセージを取り出します。続いて、アクターはこのメッセージを処理します。
最後に、処理済みのメッセージをアクターの出力ボックスに置きます。
これで、以下の結果を得られるようになります。
> loop { puts upcase.take }
AAA
BBB
CCC
しかし待ってください、loop
はまだ終了していないので無限ループになってしまいました。
このupcase
アクターは次のメッセージを待っており、receive
メソッドはアクターの受信ボックスにメッセージが届くのを待っています。メインスレッドは無限ループになっていて、アクターの出力ボックスに新しいメッセージが置かれるのを待ち構えています。
このときtake
メソッドを実行すると、次のメッセージが出力ボックスに置かれるまで実行がブロックされてしまいます。
アクターをこの例のような方法で利用すると、こういう結果になります。
🔗 アクターによる通信方法の種別
アクターは以下の2通りの方法で通信を行えます。
🔗 プッシュ型
send
メソッドとreceive
メソッドをペアで利用する- 送信側は、メッセージの送信先を認識している
- 受信側は、送信側が誰なのかを知らないので、すべてのメッセージを受け取る
send
メソッドは、受信側の無限受信ボックスのキューに到達するが、実行をブロックしないsend
メソッドはノンブロッキングであり、receive
メソッドはアクターの受信ボックスに届くあらゆるメッセージを待ち受ける- アクターベースの言語は、ほとんどの場合このプッシュ型モデルを採用している
🔗 プル型
yield
メソッドとtake
メソッドをペアで利用する- 送信側はメッセージを
yield
で生成するが、どこに到達するかは認識しない - 受信側は、誰が送信側であるかを認識しており、送信側の出力ボックスから
take
でメッセージを取得する - 受信側がメッセージを
take
で取り出せない場合、受信側の実行はブロックされる yield
もtake
もブロッキング実行である
出力ボックスは1度に1個のメッセージしか置けず、yield
メソッドはアクターの出力ボックスから直前のメッセージが取り出されるまで待機する。take
メソッドは実行をブロックし、アクターの出力ボックスからメッセージを取得可能になるまで待機する。
🔗 アクターのライフサイクル
以下の例では、アクターは受信ポートがオープンされている限り生き続けます。
r = Ractor.new do
loop do
close_incoming if receive == 0
end
"I'm done"
end
この振る舞いは以下で確認できます。
> r
=> #<Ractor:#18 (irb):93 running>
このアクターは生きていて、メッセージの到着を待ち続けています。
> r.send 123
=> #<Ractor:#18 (irb):93 running>
アクターはメッセージを受信すると、メッセージを処理して引き続き実行し続けます。
> r.send 0
=> #<Ractor:#18 (irb):93 terminated>
このシステムが終了メッセージ(ここでは0
)を受信すると、受信ポートをクローズして(つまり受信メッセージを受け取らなくなります)、アクターをterminated
とマーキングします。
終了したアクターに別のメッセージを送信しても以下のようにエラーになります。
> r.send 42
<internal:ractor>:600:in 'Ractor#send': The incoming-port is already closed (Ractor::ClosedError)
しかし、エラーが表示された後でもアクターの出力ボックスにtake
でメッセージを送信することは引き続き可能です。
irb(main):102> r.take
=> "I'm done"
この"I'm done"
(=もう無理)というメッセージが出力ボックスに置かれていたのは、アクターが次のメッセージを処理するのを諦めたためです1。アクターの無限ループがclose_incoming
メソッドで中断されたこともわかります。
アクターの出力ボックスからもう一度take
でメッセージを送信しようとすると、エラーが発生します。
> r.take
<internal:ractor>:711:in 'Ractor#take': The outgoing-port is already closed (Ractor::ClosedError)
close_incoming
の代わりにclose_outgoing
メソッド(アクターの出力ポートをクローズする)を使って同じテストを実行してみると、興味深い結果を得られます。
r = Ractor.new do
loop do
close_outgoing if receive == 0
end
"I'm done"
end
最初のうち、アクターはこれまでと同様に動き続けています。
> r
=> #<Ractor:#19 (irb):105 running>
アクターがメッセージを受け取るのも同じです。
> r.send 123
=> #<Ractor:#19 (irb):105 running>
しかしsend 0
で終了メッセージを送信しても、アクターのステートはrunning
のまま変わりません
> r.send 0
=> #<Ractor:#19 (irb):105 running>
アクターは引き続きメッセージを受け取ります。
> r.send 42
=> #<Ractor:#19 (irb):105 running>
しかしアクターでtake
を実行しても、出力ボックスから結果を取り出せません。
> r.take
<internal:ractor>:711:in 'Ractor#take': The outgoing-port is already closed (Ractor::ClosedError)
しかしエラー後もアクターは引き続き動き続けています。
irb(main):116> r
=> #<Ractor:#19 (irb):105 running>
以下のようにアクターの実行ブロック内でエラーをraiseするように変更すると、振る舞いが少し変わってきます。
r = Ractor.new do
loop do
raise "Error" if receive == 0
end
"I'm done"
end
最初のうち、アクターはこれまでと同様に受信メッセージを読み取り続けます。
irb(main):123> r.send 123
=> #<Ractor:#20 (irb):117 running>
send 0
で終了メッセージを送信すると、エラーがスローされてアクターが終了します。
irb(main):124> r.send 0
#<Thread:0x000000011fd64000 run> terminated with exception (report_on_exception is true):
(irb):119:in 'block (2 levels) in <top (required)>': Error (RuntimeError)
アクターは終了状態(terminated
)になり、メッセージを受け付けなくなります。
> r
=> #<Ractor:#20 (irb):117 terminated>
> r.send 42
<internal:ractor>:600:in 'Ractor#send': The incoming-port is already closed (Ractor::ClosedError)
アクターの出力ボックスからtake
でメッセージを取り出すこともできません。
> r.take
<internal:ractor>:711:in 'Ractor#take': thrown by remote Ractor. (Ractor::RemoteError)
🔗 まとめ
"one actor is no actor, they come in systems"2
大意: アクターは1個だけではアクターにならない。システム間で協調動作してこそ意味がある。
何しろこれはアクターの始まりに過ぎません。
🔗 関連資料
- アクターモデル - wikipedia
- Hewitt, Meijer and Szyperski: The Actor Model (everything you wanted to know...)
- The actor model in 10 minutes
- RActor class documentation
- Ractor - Ruby's Actor-like concurrent abstraction
- Introduction to Software Architecture with Actors
概要
元サイトの許諾を得て翻訳・公開いたします。
参考: アクターモデル - Wikipedia