6

私が最初にスレッドを発見したとき、通常どおり sleep を呼び出すのではなく、多くのスレッドで sleep を呼び出して、それらが実際に期待どおりに機能することを確認しようとしました。それはうまくいき、私はとても幸せでした。

しかし、私の友人は、これらのスレッドは実際には平行ではなく、睡眠がそれを偽造しているに違いないと私に言いました。

だから今、私は実際の処理を行うためにこのテストを書きました:

class Test
  ITERATIONS = 1000

  def run_threads
    start = Time.now

    t1 = Thread.new do
      do_iterations
    end

    t2 = Thread.new do
      do_iterations
    end

    t3 = Thread.new do
      do_iterations
    end

    t4 = Thread.new do
      do_iterations
    end

    t1.join
    t2.join
    t3.join
    t4.join

    puts Time.now - start
  end

  def run_normal
    start = Time.now

    do_iterations
    do_iterations
    do_iterations
    do_iterations

    puts Time.now - start
  end

  def do_iterations
    1.upto ITERATIONS do |i|
      999.downto(1).inject(:*) # 999!
    end
  end
end

run_threads() は run_normal よりもパフォーマンスが良くなかっただけでなく、さらに遅かったのです。

では、スレッドが実際には並列でないのに、なぜアプリケーションをスレッドで複雑にする必要があるのでしょうか?

** アップデート **

@fl00r は、スレッドを IO タスクに使用すればスレッドを利用できると言っていたので、さらに 2 つの do_iterations のバリエーションを作成しました。

def do_iterations
  # filesystem IO
  1.upto ITERATIONS do |i|
    5.times do
      # create file
      content = "some content #{i}"
      file_name = "#{Rails.root}/tmp/do-iterations-#{UUIDTools::UUID.timestamp_create.hexdigest}"
      file = ::File.new file_name, 'w'
      file.write content
      file.close

      # read and delete file
      file = ::File.new file_name, 'r'
      content = file.read
      file.close
      ::File.delete file_name
    end
  end
end

def do_iterations
  # MongoDB IO (through MongoID)
  1.upto ITERATIONS do |i|
    TestModel.create! :name => "some-name-#{i}"
  end
  TestModel.delete_all
end

パフォーマンス結果は同じです: 通常 > スレッド。

しかし、VM がすべてのコアを使用できるかどうかはわかりません。私がそれをテストしたときに戻ってきます。

4

5 に答える 5

7

スレッドは、IO が遅い場合にのみ高速になる可能性があります。

Ruby ではグローバル インタープリター ロックを使用しているため、一度に 1 つのスレッドしか動作できません。そのため、Ruby はどのスレッドを一度に起動するかを管理する (スレッド スケジューリング) に多くの時間を費やします。したがって、あなたの場合、IOがない場合は遅くなります!

Rubinius または JRuby を使用して、実際のスレッドを使用できます。

IO の例:

module Test
  extend self

  def run_threads(method)
    start = Time.now

    threads = []
    4.times do
      threads << Thread.new{ send(method) }
    end

    threads.each(&:join)

    puts Time.now - start
  end

  def run_forks(method)
    start = Time.now

    4.times do
      fork do
        send(method)
      end
    end
    Process.waitall

    puts Time.now - start
  end

  def run_normal(method)
    start = Time.now

    4.times{ send(method) }

    puts Time.now - start
  end

  def do_io
    system "sleep 1"
  end

  def do_non_io
    1000.times do |i|
      999.downto(1).inject(:*) # 999!
    end
  end
end

Test.run_threads(:do_io)
#=> ~ 1 sec
Test.run_forks(:do_io)
#=> ~ 1 sec
Test.run_normal(:do_io)
#=> ~ 4 sec

Test.run_threads(:do_non_io)
#=> ~ 7.6 sec
Test.run_forks(:do_non_io)
#=> ~ 3.5 sec
Test.run_normal(:do_non_io)
#=> ~ 7.2 sec

IO ジョブはスレッドとプロセスで 4 倍高速ですが、プロセスでの非 IO ジョブはスレッドと同期メソッドの 2 倍高速です。

また、Ruby にはファイバーの軽量な「コルーチン」と、非同期プロセスを処理する素晴らしいem-synchrony gemがあります。

于 2012-04-19T10:30:41.227 に答える
5

fl00r さんの言うとおり、グローバル インタープリター ロックは、Ruby で同時に複数のスレッドが実行されるのを防ぎます (IO を除く)。

このparallelライブラリは、真の並列操作に役立つ非常に単純なライブラリです。でインストールしgem install parallelます。これを使用するために書き直した例を次に示します。

require 'parallel'
class Test
  ITERATIONS = 1000

  def run_parallel()
    start = Time.now

    results = Parallel.map([1,2,3,4]) do |val|
        do_iterations
    end

    # do what you want with the results ...
    puts Time.now - start
  end

  def run_normal
    start = Time.now

    do_iterations
    do_iterations
    do_iterations
    do_iterations

    puts Time.now - start
  end

  def do_iterations
    1.upto ITERATIONS do |i|
      999.downto(1).inject(:*) # 999!
    end
  end
end

私のコンピューター (4 CPU) では、Test.new.run_normal4.6 秒Test.new.run_parallelかかりますが、1.65 秒かかります。

于 2012-04-19T11:09:33.373 に答える
4

スレッドの動作は実装によって定義されます。たとえば、JRuby は、実際のスレッドを使用する JVM スレッドでスレッドを実装します。

グローバル インタープリター ロックは、歴史的な理由からのみ存在します。もし Ruby 1.9 がどこからともなく本物のスレッドを導入したとしたら、下位互換性は失われ、その採用はさらに遅くなっていたでしょう。

Jörg W Mittagによるこの回答は、さまざまな Ruby 実装のスレッド モデル間の優れた比較を提供します。ニーズに適したものを選択してください。

そうは言っても、スレッドを使用して子プロセスが終了するのを待つことができます。

pid = Process.spawn 'program'
thread = Process.detach pid

# Later...
status = thread.value.exitstatus
于 2012-04-19T12:19:15.080 に答える
2

スレッドが並行して実行されなくても、インプロセスの cron タイプのジョブなど、いくつかのタスクを達成するための非常に効果的で簡単な方法になります。例えば:

Thread.new{ loop{ download_nightly_logfile_data; sleep TWENTY_FOUR_HOURS } }
Thread.new{ loop{ send_email_from_queue; sleep ONE_MINUTE } }
# web server app that queues mail on actions and shows current log file data

また、DRb サーバーでスレッドを使用して、Web アプリケーションの 1 つで実行時間の長い計算を処理しています。Web サーバーはスレッドで計算を開始し、すぐに Web 要求への応答を続けます。定期的にジョブのステータスを確認し、進捗状況を確認できます。詳細については、DRb Server for Long-Running Web Processes を参照してください。

于 2012-04-19T15:52:41.493 に答える
1

違いを簡単に確認するには、あまりにも多くの変数に依存する IO の代わりに Sleep を使用します。

class Test


ITERATIONS = 1000

  def run_threads
    start = Time.now
    threads = []

    20.times do
      threads << Thread.new do
        do_iterations
      end
    end

    threads.each {|t| t.join } # also can be written: threads.each &:join

    puts Time.now - start
  end

  def run_normal
    start = Time.now

    20.times do
      do_iterations
    end

    puts Time.now - start
  end

  def do_iterations
    sleep(10)
  end
end

これは、MRB でも GIL を使用したスレッド化されたソリューションとの間に違いがあります。

于 2013-03-09T08:08:49.580 に答える