マルチコア並列処理はRuby 3になる


最も長い時間の間、私はRubyでとても簡単なことをしたいと思っていました.
私は、高価なコードのブロックを並列に複数回実行し、すべてのCPUコアが点灯してください.✨
これは前にするのは非常に難しい!Rubyはマルチスレッドコードをサポートしていますが、一度に1つのスレッドだけが積極的に命令を実行することができます.それは頻繁に外部I/Oなどで待っているアプリのための罰金ですが、すべてのアプリケーションが主に懸念している場合、多くの場合は、内部のデータ処理されていません.歴史的に、あなたが本当にRubyでAsync平行しを成し遂げることができた唯一の方法は、複数のプロセスをフォークするか、バックグラウンド仕事を予定することです.
今まで.
Ruby 3でAsyncコードを実行する新しい方法です.

を、レーサー音クール.でもなんだろう?
RactorはRuby Corelibの実験的な新しいクラスです.レースでは、ルビーは初めてGILの制限を解除した.これで、複数の「Rils」を持つことができます.
レーサーは「ルビー俳優」の速記です.俳優概念は長い間、他の言語でElixRのようにコンカレンシー懸念を扱うために確立されました.基本的に、アクターは非同期で実行されるコードの単位であり、メッセージを渡して主なコードパスまたは他のアクターからデータを送受信する.ルビー俳優の歴史と概念的思考についてread this Scout APM blog post クマルHarshによって.
いくつかの中で説明されているいくつかのレースを使用して、あなたの処分で様々なパターンがありますextensive Ractor documentation .
私は、それがどのように単純であるかについて非常に感動します.私は、非同期の開発を支援する過去にスレッドや宝石で動作しようとしました、そしてそれは常に私の努力を示すために少し私の脳を痛めている.使用Ractor クラスは私が想像できるほど簡単ですasync キーワード)
私が感じ取っているもう一つは、複数のラケットから決定的で、順序のある出力を得ることがいかに簡単かということです.過去にデータを処理するスレッドを使用し、出力を配列に追加しようとした場合、配列の値は乱雑になります.スレッド1がスレッド2の後に終了すると、最終配列は2、1の順番になります.とractors.map(&:take) パターンは、1つのレーサーは、プロセスに2秒かかると別の6を取る場合でも、あなたはまだ同じ順序で値の配列で終了することが保証されている場合は、レースを起動します.

例の時間!
私はそれを考えることができたractorsの最も基本的な例を作成したい、典型的な、同期Rubyコードと比較して興味深いベンチマークでもあります.
若干の集中的なデータ処理を実行して、出力値を返す20のractorsを紡ぎだすスクリプトはここにあります、そして、最終的なスクリプト出力はすべてのレーサー出力の結合された配列です.
require "benchmark"

ractors = []
values = []

puts "Starting Ractor processing"

time_elapsed = Benchmark.measure do
  20.times do |i|
    ractors << Ractor.new(i) do |i|
      puts "In Ractor #{i}"
      5_000_000.times do |t|
        str = "#{t}"; str = str.upcase + str;
      end
      puts "Finished Ractor #{i}"
      "i: #{i}" # implicit return value, or use Ractor.yield
    end
  end

  values = ractors.map(&:take)
end

# avg: 22 seconds, 1.6x performance over not_ractors
puts "End of processing, time elapsed: #{time_elapsed.real}"

# deterministic output. nice!
puts values.join(", ")
ご覧の通り、Ractor クラスはほぼ標準的なラムダでの作業と同じくらい簡単です.任意の追加のデータ構造、スケジューリング、またはmutexesのようなスレッドの概念を介して作業を多くの精神的な頭脳を費やす必要はありません.それは「ちょうど働きます」.
というだけでなく、非-よりも高速に-Ractor -ベーススクリプト
require "benchmark"

values = []

puts "Starting Not-Ractor processing"

time_elapsed = Benchmark.measure do
  20.times do |i|
    puts "In Not-Ractor #{i}"
    5_000_000.times do |t|
      str = "#{t}"; str = str.upcase + str;
    end
    puts "Finished Not-Ractor #{i}"
    values << "i: #{i}"
  end
end

# 34.5 seconds, fans spun up !!!
puts "End of processing, time elapsed: #{time_elapsed.real}"

puts values.join(", ")
私の騙された16“MacBook Proの両方のスクリプトの実行の数の後、レーサーは1.6倍のパフォーマンスの増加を示した.RubyコードをRactorを使用するように変換する他のテストのレポートを聞いたことがあります.
Rubyスクリプトを走らせて、すべてのCPUが活動モニターで光るのを見るのは非常に刺激的です、そして、私は単コアスクリプトが私のファンを回転させたのに気づきました、その一方、マルチコア・スクリプトはファンをほとんど聞き取れませんでした.

警告
としてクールなレーサーとしては、単にスイッチを切り替えることはできませんRactor すべてのもの.オブジェクトを共有して、メッセージを通して前後にそれらを渡す方法の制限の数は、我々が現在GILを迂回していると考えて意味をなす制限を働かせます.それで、それは本当にあなたのオブジェクト、メソッドと一般的なデータ構造(特に自然の「グローバル」であるオブジェクト)を構築する方法の全く新しいレベルを必要としません.私はすぐに仕事を望んでいる何かの例として、私は最近、2009年にコンテンツパイプラインの書き直しを開始Bridgetown (静的サイトジェネレータ).Bridgetownがサイトを処理しているとき、メモリ内の共有オブジェクトの数は、特にサイトオブジェクトと一連のコレクションオブジェクトがあります.通常、特定のページ/ポスト/etcがロードされるとき、それはサイトまたはコレクションの必要な配列にそれ自体を加えます.レーサーと、あなたはそれを行うことはできません!並列に走る複数の並行したractorは、直接共有状態を変更することができません.代わりに、すべてのプロセスを複数の段階に分割する必要があります:ページをロードするために必要なメタデータを収集し、その後、すべてのローディングロジックを実行するためにラケットをスピンしてから、ロードされたページを収集して、共有オブジェクトに追加するメッセージを渡します.
それはとにかく理論だ.もしそれが機能しているなら、(a)を報告しなければなりません.また、(b)通常の同期コードに対する性能改善ならば.しかし、約束はそこにあります:あなたのアプリまたは宝石をレーサー概念のまわりに建設することによって、あなたのRubyコードは、一旦潜在的に記念碑的なパフォーマンス増加をもたらすすべてのCPUコアに集中的な活動をシャトルにする能力を得ます.

結論
ルビー3はとても新しいのでRactor 実験的にマークされていますが、ルビーの生態系がこのエキサイティングな新しい方向に進化するのに時間がかかると思います.そして、それは解決するために難解なractorのバグやgotchasのいくつかのポイントのリリースを取ることがあります.しかし、私はこれが起こるということを疑いません.報酬は、あまりに長くテーブルの上に残るために、あまりに苛立ちます.最後に、エリクサーやゴーのような他の言語を見ることができます.そして、コンコードを書くことがどんなに簡単であるかについて賢明にため息をつく代わりに、我々は我々のそでを巻き上げることができて、いくつかのラケットを発射して、それらのCPUコアを照らすのを見ることができます.
あなたのルビーサイトやアプリケーションに直接適用することができますタイムリーなヒントを受信するには?Subscribe to RUBY3.dev today 将来的に、この楽しいと強力な言語でウィズになる.