zenet_logo

-株式会社ゼネット技術ブログ-

Ruby 並列処理を比較!

はじめに

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 も導入されています。

README

特徴

  • 基本は使用パターンに応じて、Process, Thread, Ractor と同じ特徴。
  • 使い方が共通化されているため、各機能特有の書き方を気にする必要がない。
  • 古い gem のため、機能や使用法の紹介記事が多い。
  • バージョンアップ頻度はやや低い。

gem async

async は、Fiber を利用した非同期 I/O 向きの gem です。
Fiber の独特の書き方を気にすることなく、Fiber ベースの疑似並列処理の恩恵を受けることができます。
特に書きやすさを重視している傾向があり、公式ドキュメントが非常に充実しているのが特徴と言えます。

README

特徴

  • 基本は Fiber ベース。メモリ軽量で非同期 I/O に向く。
  • 公式ドキュメントが充実。
  • コミュニティが活発で、バージョンアップ頻度は高い。

まとめ

用途に応じた使い分けをまとめました。

用途 手段
重い計算処理などを並列化 Process, Ractor, parallel
通信やファイル I/O Thread
非同期 I/O や軽量な並行タスク async, Fiber
複数のユースケースに一本で対応 parallel
Ruby の機能で安全かつ高速な処理 Ractor

想像より多くの手段があることに驚きました。
どれを使うにしても、要件に応じて最適な手法を吟味することが大切ですね。


参考