如果 Ruby 线程不是真正并行的,我能给它们什么用



当我第一次发现线程时,我尝试通过在许多线程中调用睡眠而不是正常调用睡眠来检查它们是否真的按预期工作。它奏效了,我很高兴。

但后来我的一个朋友告诉我,这些线并不是真正平行的,睡眠一定是假的。

所以现在我写了这个测试来做一些真正的处理:

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 任务,我可以利用线程,所以我又写了两个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 是否能够使用所有内核。当我测试过后会回来的。

只有当你有一些慢的IO时,线程才能更快。

在 Ruby 中,你有全局解释器锁,所以一次只能有一个线程工作。因此,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 作业比线程和同步方法快两倍。

同样在Ruby中展示了Fibers轻量级的"Corutines"和令人敬畏的em-synchrony宝石来处理异步进程

> 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_normal需要 4.6 秒,而Test.new.run_parallel需要 1.65 秒。

线程

的行为由实现定义。例如,JRuby使用JVM线程实现线程,而JVM线程又使用真正的线程。

全局解释器锁只是出于历史原因才存在。如果Ruby 1.9只是凭空引入真正的线程,那么向后兼容性就会被破坏,并且会进一步减慢其采用速度。

Jörg W Mittag 的这个答案提供了各种 Ruby 实现的线程模型之间的出色比较。选择一种适合您需求的产品。

话虽如此,线程可用于等待子进程完成:

pid = Process.spawn 'program'
thread = Process.detach pid
# Later...
status = thread.value.exitstatus

即使线程不并行执行,它们也可以是完成某些任务的非常有效、简单的方法,例如进程内 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 应用程序的长时间运行的计算。Web 服务器在线程中启动计算,并立即继续响应 Web 请求。它可以定期查看作业的状态,并查看其进度。有关更多详细信息,请阅读用于长时间运行的 Web 进程的 DRb 服务器。

对于查看差异的简单方法,请使用 Sleep 而不是 IO,后者也依赖于太多变量:

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 之间也会有所不同

最新更新