Ractorの概要
RactorはActorモデルのRuby実装。 以前からRevactorというのがあったけど、それとはだいぶ違う。
Ractorは現状experimentalな組み込みライブラリで、doc.ruby-lang.orgのドキュメントはまだない。
要点は次のとおり
Ractor.new { ... }でRactor生成。ブロック内が実行される。引数を渡すとそのままブロック引数になる (ブロック外のオブジェクトへの直接アクセスは不可)- Ractor内で
Ractor.yieldしたら、Ractor#takeで取り出せる Ractor#sendしたのをRactor内でRactor.receiveで取り出せるsendは非同期で常時即時リターン。takeは同期なのでブロッキングされるRactor.selectすると複数のRactorからどれかが終わるのを待てる。終わったRactorオブジェクトと、ブロックの値が返る- 回収はしなくても良いが、メインが終わるとRactorは打ち切られてしまうので、メインが終わるタイプは待ち合わせが必要
キューを作る場合はRactor.yieldするRactorを作って他のRactorに渡すのがよさそう。
q = (1..20).to_a
rs = []
qer = Ractor.new q do |q|
loop do
puts "QIN"
Ractor.yield q.shift
end
end
4.times do |i|
rs.push(Ractor.new(qer, i) {|qer, i|
item = qer.take
puts "R#{i} received #{item}"
sleep i
item
})
end
loop do
break if rs.empty?
r, msg = Ractor.select(*rs)
rs.delete(r)
if msg
rs.push(Ractor.new(qer, 5) {|qer, i|
item = qer.take
puts "R#{i} received #{item}"
sleep i
item
})
end
end複数のどれかにsendすることができないので、双方向マルチワーカーは難しい。 方法としては、司令塔になるRactorに対して自分が準備できたことを知らせるために「自分自身を」sendするというものがある。
通常、sendしたオブジェクトはmoved objectという扱いになって、sendした側では扱えないのだが、selfをsendする分には問題ないようだ。
qs = (1..30).to_a
workers = []
master = Ractor.new qs do |qs|
loop do
r = Ractor.receive
r.send(qs.shift)
end
end
3.times do |i|
workers.push(Ractor.new(master) do |master|
loop do
master.send self
v = Ractor.receive
break unless v
puts "#{self.object_id} got #{v}"
end
end)
end
until workers.empty?
r, msg = Ractor.select(*workers)
workers.delete(r)
endAVIF圧縮を並列化してみる
キューがあればだいたいの並列事案は解決する。
photocompress.zshの並列化をやってみよう。
AVIF圧縮はちょっと時間がかかり、なおかつシングルスレッドで動くので並列化したいという気持ちは割とあった。
ImageMagickとかもいい材料かも。
実はphotocompress.zshは動作に少し問題がある1のでアプローチを変更する。
作業ディレクトリ上の画像ファイルを変換したものをアルバムへ、サムネイルをサムネイルへ出力。
Ractorを中心にRactorの流れをまとめるとこうなる。
setup
master = Ractor.new files, SETTINGS, STDERR do |files, settings, stderr|
loop do
i = files.shift
unless i
Ractor.yield [nil, nil]
next
end
# ...Classifying...
Ractor.yield [i, type]
end
end
rs = []
workers.times do
rs.push(Ractor.new(master, SETTINGS, CONFIG) do |master, settings, config|
loop do
item = master.take
break nil unless item[0]
# ...Compress...
end
end)
end
until rs.empty?
r, msg = Ractor.select(*rs)
rs.delete(r)
end
# ...JPEG Optimization...先の例の1つ目に近い形だが、先の例では1処理ごとにRactorは終わる形だったが、ループして最後まで取って、取りきったら終わる形にして待ち合わせた。
Ractorの感想
Rubyの並列処理の中では感触はかなり良いほうで、Rubyで投げっぱなし並列処理を書きやすい。
ただ、プロセス自体が終わってしまう場合は結局待ち合わせを必要とするため、完全に投げっぱなしにはできず、使い勝手はそこまでよくない。
というのも、JavaScriptのasync/awaitのような感覚で使うことも可能2なのだけど、最終的に全部待たないといけないというのはなんとも微妙な書き方になる。
どちらかというとスレッド間通信がメインになると思うんだけども、ThreadやFiberと比べれば筋が良い。
いままでだとThreadとRevActorみたいな感じだったけれど、正直いまいちだった。
というのも、Threadは結局マルチスレッドにならないし、一方Fiberはコントロールが難しい。
制限はあるものの利用環境が整っているRactorはかなり使いやすいと感じた。
一方で、最大の難点のその制限。
STDERRにすらアクセスできないため、Ractorのブロックで書ける内容は非常に限定的で、小さいものにならざるをえない。
また、呼び出したメソッドの内部でSTDERRにアクセスしていてもエラーになるし、そうしたエラーは結局、Ractorを使うことで「本来隠蔽されているはずの内部事情を意識させる」ものになるから、すごく筋の悪い状態になってしまう。
Experimentalを外す段階でもこの制限はそのままなのだろうか。
今回のプログラムはsystemを呼んでいるためThreadでもマルチスレッドを活用できるが、マルチコアパワーを活用するのに新たな選択肢と考えても良さそうだ。
ただし。ただし、である。 マルチプロセスのほうが良い。間違いない。