RubyのRactorで並列AVIF圧縮
開発::util
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に渡すのがよさそう。
= (1..20).to_a
q = []
rs
= Ractor.new q do |q|
qer loop do
puts "QIN"
Ractor.yield q.shift
end
end
4.times do |i|
.push(Ractor.new(qer, i) {|qer, i|
rs= qer.take
item puts "R#{i} received #{item}"
sleep i
item})
end
loop do
break if rs.empty?
= Ractor.select(*rs)
r, msg .delete(r)
rsif msg
.push(Ractor.new(qer, 5) {|qer, i|
rs= qer.take
item puts "R#{i} received #{item}"
sleep i
item})
end
end
複数のどれかにsendすることができないので、双方向マルチワーカーは難しい。 方法としては、司令塔になるRactorに対して自分が準備できたことを知らせるために「自分自身を」sendするというものがある。
通常、sendしたオブジェクトはmoved
objectという扱いになって、sendした側では扱えないのだが、self
をsendする分には問題ないようだ。
= (1..30).to_a
qs = []
workers
= Ractor.new qs do |qs|
master loop do
= Ractor.receive
r .send(qs.shift)
rend
end
3.times do |i|
.push(Ractor.new(master) do |master|
workersloop do
.send self
master= Ractor.receive
v break unless v
puts "#{self.object_id} got #{v}"
end
end)
end
until workers.empty?
= Ractor.select(*workers)
r, msg .delete(r)
workersend
AVIF圧縮を並列化してみる
キューがあればだいたいの並列事案は解決する。
photocompress.zsh
の並列化をやってみよう。
AVIF圧縮はちょっと時間がかかり、なおかつシングルスレッドで動くので並列化したいという気持ちは割とあった。
ImageMagickとかもいい材料かも。
実はphotocompress.zsh
は動作に少し問題がある1のでアプローチを変更する。
作業ディレクトリ上の画像ファイルを変換したものをアルバムへ、サムネイルをサムネイルへ出力。
Ractorを中心にRactorの流れをまとめるとこうなる。
setup
= Ractor.new files, SETTINGS, STDERR do |files, settings, stderr|
master loop do
= files.shift
i unless i
Ractor.yield [nil, nil]
next
end
# ...Classifying...
Ractor.yield [i, type]
end
end
= []
rs
.times do
workers.push(Ractor.new(master, SETTINGS, CONFIG) do |master, settings, config|
rsloop do
= master.take
item break nil unless item[0]
# ...Compress...
end
end)
end
until rs.empty?
= Ractor.select(*rs)
r, msg .delete(r)
rsend
# ...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でもマルチスレッドを活用できるが、マルチコアパワーを活用するのに新たな選択肢と考えても良さそうだ。
ただし。ただし、である。 マルチプロセスのほうが良い。間違いない。