【问题标题】:What use can I give to Ruby threads, if they are not really parallel?如果 Ruby 线程不是真正并行的,我能给它们带来什么用处?
【发布时间】:2012-04-30 20:03:02
【问题描述】:

当我第一次发现线程时,我尝试通过在多个线程中调用 sleep 来检查它们是否确实按预期工作,而不是正常调用 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 任务,我可以利用它们,所以我又写了两个 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

性能结果还是一样:正常>线程。

但现在我不确定我的虚拟机是否能够使用所有内核。等我测试完再回来。

【问题讨论】:

  • 线程糟透了,纤维摇滚! :D
  • 用官方的ruby,线程是伪造的,但是对于jruby和rubinius,我相信是真正的线程。
  • 性能并不是使用线程的唯一原因。看我的回答。

标签: ruby multithreading parallel-processing


【解决方案1】:

只有在 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 轻量级“协程”和很棒的 em-synchrony gem 来处理异步进程

【讨论】:

    【解决方案2】:

    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 秒。

    【讨论】:

    • 哇,我不知道那个宝石。我试试看
    • @HappyDeveloper 请注意,默认情况下它会使用管道作为交换机制生成进程。它不是线程,也不是轻量级的。如果您将:in_threads 选项与普通 Ruby 一起使用,我怀疑您是否有任何优势。
    【解决方案3】:

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

    Global Interpreter Lock 仅出于历史原因而存在。如果 Ruby 1.9 只是突然引入了真正的线程,那么向后兼容性就会被破坏,并且会进一步减慢它的采用速度。

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

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

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

    【讨论】:

      【解决方案4】:

      即使线程不并行执行,它们也可以是完成某些任务的一种非常有效、简单的方法,例如进程内 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 请求。它可以定期查看作业的状态并查看其进展情况。更多详情,请阅读DRb Server for Long-Running Web Processes

      【讨论】:

        【解决方案5】:

        要查看差异的简单方法,请使用 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 之间有所不同

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-09-15
          • 2010-12-27
          • 2012-08-12
          • 2016-03-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多