- Ruby 4.0 の Ractor 使ってみた (1) 触ってみるまで
- Ruby 4.0 の Ractor 使ってみた (2) 題材
- Ruby 4.0 の Ractor 使ってみた (3) Ractor::Port に馴染む
- Ruby 4.0 の Ractor 使ってみた (4) Ractor に馴染む
- Ruby 4.0 の Ractor 使ってみた (5) 設計方針
- Ruby 4.0 の Ractor 使ってみた (6) 実装
- Ruby 4.0 の Ractor 使ってみた (7) ベンチマークテスト
大方針
以下のような方針で Ractor 化してみる。これは,Ractor について解説した複数の記事を参考に試行錯誤した結果なのだが,何を見てどういう過程を経てここに辿り着いたのか思い出せない。
- CPU のコア数に応じた数の Ractor を用意する
- これを労働者と呼ぼう
- ワーカーに仕事を割り振るためのポート(Ractor::Port)を一つだけ用意する
- これをコントローラーと呼ぼう
- 処理結果を受け取るポートを一つだけ用意する
- これを結果ポートと呼ぼう
- 各ワーカーは,処理すべき値を受け取ってはその結果を結果ポートに送る
- これを無限に繰り返す
- ワーカーの具体的な動作は後述
- 結果ポートから値を取り出す
作る Ractor の数を「CPU のコア数に応じた数」と書いたが,必ずしも「CPU のコア数と同じ数」というわけではない。
あとで分かったことだが,私の環境ではコア数よりも少ない数にしたほうが効果的だった,これについてはのちの記事に書く。
CPU のコア数は,etc ライブラリーを使うと Etc.nprocessors で知れるが,その値をそのまま使わないほうがよさそうだ。
「ワーカー」「コントローラー」はこういう場合の一般的な名前のようだが,「結果ポート」は私が仮にそう呼ぶだけで,識者ならもっと適切な名前を付けるかもしれない。
コントローラーを使う側
コントローラーを使う側は以下のように記述する。
処理したい値の配列が values という変数に入っているとしよう。
values の各要素について以下のことを行う。
- コントローラーから「いま暇にしているワーカー」を一つ受け取る
- そのワーカーに対し,値を送り込む
values の全部の要素を送り終わったら,こんどは values の要素数だけ「結果ポート」から値を受け取る。
気をつけなくてはならないのは,並列処理の特性上,ワーカーに値を送り込んだ順に結果ポートから値が受け取れるわけではないことだ。
そのため,ワーカーに対しては,処理してもらいたい値とともに,それが何番目の要素であるかというインデックスも渡してやる:
values.each_with_index do |value, index|
# ここで,コントローラーから「暇なワーカー」を一つ教えてもらう
# そのワーカーに `value` と `index` のペアを渡す
end
結果ポートから受け取るのも,インデックスと処理結果の組にしなければならない。
インデックス情報があれば順序が入力と同じ「結果の配列」を得ることができる。
ところで,コントローラーが「ワーカーに仕事を割り振る」といっても,コントローラー自身が「暇そうなワーカーはどれかな?」と探すわけではない。
コントローラーはただのポートだしね。オブジェクトを受け渡すことしかできない。
ここがどうなっているかは次節参照。
ワーカーの動作
ワーカーは,以下のことを無限に繰り返す。
- コントローラー(と名付けられたポート)に自身を送る
- デフォルトポートから「インデックスと処理してほしいオブジェクト」の組を受け取る
- 結果ポートに「インデックスと処理結果のオブジェクト」の組を送る
何となく
- 労働者 が 仕事紹介所 に申込をする
- 労働者の スマホ に仕事依頼が届く
- 完成した仕事を 決まった場所 に納品する
を無限に繰り返しているような図が浮かぶ。
汎用化
試作を繰り返してみて分かったが,上述したような処理を書くには 20〜30 行程度のコードが必要なようだ。
並列化前に
result = texts.map{ char_histogram(_1) }
と 1 行で書けていたものがいつも何十行にもなるのは嫌だな。
そこで,上述のような仕組みをクラスにまとめることにする。
使うときは
processor = RactorFilterProcessor.new(4) do |text|
char_histogram(text)
end
result = processor.filter(texts)
のように書くことにしたい。
RactorFilterProcessor.new の引数は,ワーカーとなる Ractor の数。
ブロックは,要素ごとにやらせたい変換処理を書く。
なお,クラス名,メソッド名に filter という英単語を使っているが,妥当かは分からない1。processor という名前も適切か分からない。
次回はいよいよ実装だ。
-
filter はもともと濾過器であって,特定のものだけを通す働きをするものだが,コンピューター業界ではテキストなどのデータに加工を施すものをそう呼び,さらには入力に応じた出力を返すもの全般をそう呼ぶように意味が拡大してきている気がする(知らんけど)。参照:違和感のある計算機用語 - Qiita ↩