
はじめに
Ruby で重い処理に悩まされていませんか?分割して並列処理ができたらな、なんてことありませんか?
「Ruby は並列処理に向いていない言語」と言われていたのも昔の話、今は様々な選択肢がサポートされています!
今回は Ruby 組み込みライブラリである Process, Thread, Fiber, Ractor、
そして代表的な gem であるparallel, asyncについて調べてみました。
Ruby における並列処理
Ruby はそもそもグローバル VM ロック(GVL, または GIL)と呼ばれるロック機構があり、完全なマルチスレッドでの実行ができません。
このため、Ruby で並列処理を行うためには
- マシンのプロセスを確保し、マルチプロセスで実行する(Process)
- スレッド自体は作成するが、コード実行自体は 1 本ずつ。I/O 待機などは並列で実行する(Thread)
- 同一スレッド内で疑似的にマルチスレッドのようにふるまう(Fiber)
という手法が用いられてきました。
しかし Ruby 3.0 で登場した Ractor では、ついにスレッドベースでの並列処理が利用可能になりました!
ライブラリ・gem の紹介
各手法の特徴を解説します。
サンプルコードは簡単なものですが、実際に実行してみると並列での動きがわかると思います。
細かい仕様はリファレンスをご覧ください。
module Process
Process は組み込みライブラリであり、マシンのプロセスを管理するためのモジュールです。
プロセスの状態管理のほか、プロセスを生成して並列処理ができます。
特徴
- マルチプロセスで実行できるため、非常に高速。
- プロセス確保によるメモリ使用量は大きい。
- マシンのプロセスを使用するため、実行環境に左右されやすい。
サンプルコード
pid = Process.fork do puts "子プロセス: PID=#{Process.pid}" sleep 2 puts "子プロセス終了" end puts "親プロセス" Process.wait(pid) puts "親プロセス終了" => 親プロセス 子プロセス: PID=695, 親PID=52 子プロセス終了 親プロセス終了
class Thread
Thread は組み込みライブラリであり、スレッドを利用するためのクラスです。
同一プロセス内で複数のスレッドを利用し、並列実行ができます。
特徴
- 同一プロセス内のため、メモリ使用量が少ない。
- 前述の GVL のため、完全な並列実行はできない。
- I/O 待機などでは GVL が解放されるため、このタイミングではマルチスレッドで処理ができる。
- 通信やファイル I/O など、待ち時間が多い処理に向いている。
サンプルコード
threads = 3.times.map do |i| Thread.new do puts "Thread #{i} start" sleep 1 puts "Thread #{i} end" end end threads.each(&:join) => Thread 0 start Thread 1 start Thread 2 start Thread 1 end Thread 0 end Thread 2 end
class Fiber
Fiber は組み込みライブラリであり、軽量な疑似スレッドを利用するためのクラスです。
マルチスレッドというよりは、タスクの切り分け、コルーチン化が目的となります。
ノンプリエンティブ(処理を実行するコンテキストを、コード内で明示的に切り替えること)であることが最大の特徴です。
特徴
- 同一スレッドのため、メモリ軽量。
- 非同期 I/O(入力完了を待たずに逐次出力する処理)に向いている。
- 明示的に親子のコンテキストを切り替えながら実行する。
- この切り替えを適切に利用することで、親子間でデータの行き来をさせながら処理ができる。
- 他と比べて書き方が独特。慣れるまで難しいかも…。
サンプルコード
f = Fiber.new do puts "Fiber start" Fiber.yield puts "Fiber resume" end f.resume => Fiber start f.resume => Fiber resume
class Ractor
Ractor は Ruby3 系から実装された組み込みライブラリであり、マルチスレッドでの並列処理を行うためのクラスです。
前述の GVL によるマルチスレッド実行の制限を回避し、完全な並列処理を軽量に利用できます。
Ruby3.4 現在でもまだ試験実装の段階であり、今後の用法の確立が期待されます。
特徴
- 完全な並列処理のため、非常に高速。
- マルチプロセスではないためメモリも軽量。
- スレッドセーフを維持したまま並列処理ができるため、データ競合などのリスクがない。
サンプルコード
r1 = Ractor.new { 10.times.map { |i| i**2 } } r2 = Ractor.new { 10.times.map { |i| i**3 } } p r1.take => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] p r2.take => [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
gem parallel
parallel は、並列/並行処理を簡単に行える gem です。
Process, Thread の機能を簡易化・共通化して利用できるようにまとめてあり、用途に応じて使い分けることができます。
試験的に Ractor も導入されています。
特徴
- 基本は使用パターンに応じて、Process, Thread, Ractor と同じ特徴。
- 使い方が共通化されているため、各機能特有の書き方を気にする必要がない。
- 古い gem のため、機能や使用法の紹介記事が多い。
- バージョンアップ頻度はやや低い。
gem async
async は、Fiber を利用した非同期 I/O 向きの gem です。
Fiber の独特の書き方を気にすることなく、Fiber ベースの疑似並列処理の恩恵を受けることができます。
特に書きやすさを重視している傾向があり、公式ドキュメントが非常に充実しているのが特徴と言えます。
特徴
- 基本は Fiber ベース。メモリ軽量で非同期 I/O に向く。
- 公式ドキュメントが充実。
- コミュニティが活発で、バージョンアップ頻度は高い。
まとめ
用途に応じた使い分けをまとめました。
| 用途 | 手段 |
|---|---|
| 重い計算処理などを並列化 | Process, Ractor, parallel |
| 通信やファイル I/O | Thread |
| 非同期 I/O や軽量な並行タスク | async, Fiber |
| 複数のユースケースに一本で対応 | parallel |
| Ruby の機能で安全かつ高速な処理 | Ractor |
想像より多くの手段があることに驚きました。
どれを使うにしても、要件に応じて最適な手法を吟味することが大切ですね。
参考
