Rubyでスレッドを使用する

多くのRuby開発者はスレッドを無視しますが、これは非常に便利なツールです。 この記事では、RubyでのIOストリームの作成を検討し、Rubyが多くの計算操作が行われるフローに対処する方法を示します。 別のRuby実装を適用して、 DRbモジュールを使用してどのような結果が得られるかを調べてみましょう。 記事の最後で、 Ruby on Railsアプリケーションのさまざまなサーバーでこれらの原則がどのように使用されるかを説明します。



RubyのIOフロー



小さな例を考えてみましょう:



def call_remote(host) sleep 3 #      end
      
      





たとえば、キャッシュをクリアするために2つのサーバーにアクセスする必要がある場合、この関数を連続して2回呼び出します。



 call_remote 'host1/clear_caches' call_remote 'host2/clear_caches'
      
      





その後、プログラムは6秒間動作します。



たとえば、次のようにスレッドを使用すると、プログラムの実行を高速化できます。



 threads = [] ['host1', 'host2'].each do |host| threads << Thread.new do call_remote "#{host}/clear_caches" end end threads.each(&:join)
      
      





2つのスレッドを作成し、各スレッドでサーバーに接続し、 #joinコマンドは、メインプログラム(メインスレッド)が終了するまで待機する必要があると述べました。 これで、プログラムは3秒で2倍の速度で正常に実行されます。



より多くのフローが良好で異なる


より複雑な例を考えてみましょう。この例では、提供されているAPIを介して、 Jekyllプロジェクトに関するGitHubのすべてのクローズされたバグと問題を取得しようとします



GitHubに対してDoS攻撃を行いたくないので、同時スレッドの数を制限し、それらを計画し、起動し、利用可能になったときに結果を収集する必要があります。



標準のRubyライブラリには、このような問題を解決するための既製のツールが用意されていないため、 Rubyでスレッドグループを作成するためのFutureProofライブラリを実装しました。



その原理は単純です-同時スレッドの最大許容数を指定して、新しいグループを作成する必要があります。



 thread_pool = FutureProof::ThreadPool.new(5)
      
      





タスクを追加します:



 thread_pool.submit 2, 5 do |a, b| a + b end
      
      





そしてその意味を尋ねます:



 thread_pool.values
      
      





したがって、 Jekyllプロジェクトについて必要な情報を取得するには、次のコードで十分です。



 require 'future_proof' require 'net/http' thread_pool = FutureProof::ThreadPool.new(5) 10.times do |i| thread_pool.submit i do |page| uri = URI.parse( "https://api.github.com/repos/mojombo/jekyll/issues?state=close&page=#{page + 1}&per_page=100.json" ) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.request(Net::HTTP::Get.new(uri.request_uri)).body end end thread_pool.perform puts thread_pool.values[3] # [{"url":"https://api.github.com/repo ...
      
      





FutureProofライブラリの実装はQueueクラスに基づいており、複数のスレッドで安全に動作するキューを作成できます。これにより、複数のスレッドが互いに値を同時にキューに書き込まず、同時に同じ値を考慮しないようにします。



ライブラリは例外処理も開発しました-スレッドの実行中にこれが発生した場合、 thread_poolは受け取った値から配列を返すことができ、プログラマーが配列の特定の要素に直接アクセスしようとした場合にのみ例外をスローします。



スレッドグループの実装は、部分的にインスピレーションを受けたJavajava.util.concurrentにより近いスレッドで動作するRubyの能力をもたらす試みです。



FutureProofライブラリを使用すると、 IOストリームの操作に関連するタスクをはるかに便利かつ効率的に実行できます。 このライブラリは、 Rubinius 1.9.3、2.0Rubiniusをサポートしています。



スレッドとコンピューティング



スレッドを使用してプログラムのパフォーマンスを向上させる成功経験を考慮して、2つのテストを実行します。1つは2回連続して階乗1000を計算し、もう1つは並行して実行します。



 require 'benchmark' factorial = Proc.new { |n| 1.upto(n).inject(1) { |i, n| i * n } } Benchmark.bm do |x| x.report('sequential') do 10_000.times do 2.times do factorial.call 1000 end end end x.report('thready') do 10_000.times do threads = [] 2.times do threads << Thread.new do factorial.call 1000 end end threads.each &:join end end end
      
      





その結果、( Ruby 2.0を使用して)予想外の結果が得られました-並列実行には2秒以上かかりました:



  user system total real sequential 24.130000 1.510000 25.640000 (25.696196) thready 24.600000 2.420000 27.020000 (26.877708)
      
      





その理由の1つは、スケジューリングスレッドを使用してコードを複雑にしたことと、2番目のRubyが一度に1つのコアのみを使用してこのプログラムを実行したことです。 残念ながら、Rubyが1つのrubyプロセスに複数のコアを使用するように強制する機会は現在提供されていません。



jRuby 1.7.4で同じスクリプトを実行した結果を示します。



  user system total real sequential 33.180000 0.690000 33.870000 (33.090000) thready 37.820000 3.830000 41.650000 (24.333000)
      
      







ご覧のとおり、結果は良くなっています。 なぜなら 測定は2つのコアを備えたコンピューターで行われ、コアの1つが75%しか使用されなかったため、改善は200%ではありませんでした。 しかし、したがって、コアの数が多いコンピューターでは、さらに多くの並列スレッドを実行し、結果をさらに向上させることができます。



jRubyJVMでのRubyの代替実装であり、言語自体に非常に優れた機能をもたらします。



同時スレッドの数を選択する場合、パフォーマンスを損なうことなく、1つのコアでIO操作に関与する多くのスレッドを配置できることを覚えておく必要があります。 ただし、計算処理の場合、スレッドの数がコアの数を超えると、パフォーマンスが少し低下します。



オリジナルのRuby実装( MRI )の場合、計算操作に1つのスレッドのみを使用することをお勧めします。 スレッドとの真の並列処理を実現するには、 jRubyRubiniusを使用する必要があります。



プロセスレベルの同時実行性



現在わかっているように、1つのrubyプロセス( Unixシステム)のRuby MRIは 、一度に1つのコアのリソースしか使用できません。 この欠点を回避する方法の1つは、次のようなプロセスの分岐を使用することです。



 read, write = IO.pipe result = 5 pid = fork do result = result + 5 Marshal.dump(result, write) exit 0 end write.close result = read.read Process.wait(pid) puts Marshal.load(result)
      
      







プロセスのフォークは、作成時に結果変数の値を5にコピーしますが、メインプロセスはフォーク内の変数のそれ以上の変更を認識しないため、 IO.pipeを使用してフォークとメインプロセスの間にメッセージを確立する必要がありました。



この方法は効果的ですが、面倒で不便です。 配布プログラミングにDRbモジュールを使用すると、より興味深い結果を得ることができます。



プロセスの同期にはDRbモジュールを使用します





DRbモジュールは標準のRubyライブラリの一部であり、配布プログラミング機能を担当します。 彼のアイデアの基礎は、ネットワーク上の任意のコンピューターに1つのRubyオブジェクトへのアクセスを許可する能力です。 このオブジェクトを使用したすべての操作の結果、その内部状態は、接続されているすべてのコンピューターに表示され、常に同期されます。 一般に、モジュールの機能は非常に幅広く、別の記事に値します。



Rinda :: TupleSpaceタプルをこのDRb機能と一緒に使用して、メインプログラムコンピューターと他の接続されたマシンの両方で別々のプロセスでコードを実行するPthreadモジュールを作成するというアイデアを思いつきました。 Rinda :: TupleSpaceは名前によるタプルへのアクセスを提供し、 Queueオブジェクトのように、一度に1つのスレッドまたはプロセスのみにタプルを読み書きできます。



したがって、 Ruby MRIがいくつかのコアでコードを実行できるソリューションが登場しました。



 Pthread::Pthread.new queue: 'fact', code: %{ 1.upto(n).inject(1) { |i, n| i * n } }, context: { n: 1000 }
      
      







お気づきかもしれませんが、実行する必要があるコードは文字列として提供されます。 プロシージャの場合、 DRbは別のプロセスにリンクのみを転送し、その実行のために、このプロシージャを作成したプロセスのリソースを使用します。 メインプロセスのコンテキストを取り除くために、コードを文字列として他のプロセスに送信し、追加のディクショナリの文字列変数の値を送信します。 追加のマシンをコード実行に接続する方法の例はプロジェクトのホームページにあります



Pthreadライブラリは、 MRIバージョン1.9.3および2.0をサポートしています。



Ruby on Railsの並行性



Ruby on Railsのサーバーとバックグラウンドタスクを実行するライブラリは、2つのグループに分けることができます。 1つ目は、フォークを使用してユーザー要求を処理するか、バックグラウンドタスクを実行します(追加プロセス)。 したがって、 MRIとこれらのサーバーおよびライブラリを使用して、複数の要求を一度に処理し、複数のタスクを同時に実行できます。



ただし、この方法には欠点があります。 プロセスのフォークは、プロセスを作成したプロセスのメモリをコピーするため、3人の「ワーカー」を持つUnicornサーバーは1GBのメモリを占有し、ほとんど作業を開始できません。 Resqueなどのバックグラウンドタスクを実行するライブラリについても同様です。



Ruby on Rails用のPumaサーバーの作成者は、 jRubyRubiniusの機能を考慮に入れ、主にこれら2つの実装に焦点を合わせたサーバーをリリースしました。 同じUnicornとは異なり、 Pumaは要求を同時に処理するために必要なメモリがはるかに少ないスレッドを使用します。 したがって、 jRubyまたはRubiniusと組み合わせて使用​​すると、 Pumaは優れた代替手段になります。 したがって、Sidekiqライブラリは原則として3倍になります



おわりに





スレッドは非常に強力なツールで、特に長時間のIO操作や計算に関しては、いくつかのことを一度に行うことができます。 この記事では、Rubyとそのさまざまな実装の可能性と制限のいくつかを検討し、2つのサードパーティライブラリを使用してストリームの操作を簡素化しました。



したがって、この記事の著者は、 Ruby 、スレッドをいじることをお勧めします。将来のRailsプロジェクトを開始するときは、代替実装jRubyRubiniusに目を向けてください。



All Articles