【问题标题】:Ruby Timeout Module - Timeout doesn't executeRuby 超时模块 - 超时不执行
【发布时间】:2015-02-22 02:21:36
【问题描述】:

我在 Ruby 中发现了 Timeout 模块,并想对其进行测试。我在http://ruby-doc.org/stdlib-2.1.1/libdoc/timeout/rdoc/Timeout.html查看了他们的官方源代码

这是我的代码

require 'timeout'
require 'benchmark'
numbers = [*1..80]
Timeout::timeout(5) { numbers.combination(5).count }

=> 24040016

我做了一些基准测试,得到了以下结果。

10.828000      0.063000     10.891000      11.001676

根据文档,如果该块未在 5 秒内执行,则此方法应该返回异常。如果在时间范围内执行,则返回代码块的结果

对于它的价值,我尝试了 1 秒而不是 5 秒的超时,但我仍然得到返回代码块的结果。

这里是官方文档

timeout(sec, klass=nil)
Performs an operation in a block, raising an error if it takes longer than sec seconds to complete.

sec: Number of seconds to wait for the block to terminate. Any number may be used,
including Floats to specify fractional seconds. A value of 0 or nil will execute the
block without any timeout.

klass: Exception Class to raise if the block fails to terminate in sec seconds. Omitting
will use the default, Timeout::Error

我很困惑为什么这不起作用。

【问题讨论】:

    标签: ruby exception timeout


    【解决方案1】:

    问题在于 MRI(Matz 的 Ruby 实现)线程调度的工作方式。 MRI 使用 GIL(全局解释器锁),这实际上意味着一次只有一个线程真正运行。

    有一些例外,但在大多数情况下,任何时候只有一个线程在执行 Ruby 代码。

    通常您不会注意到这一点,即使在消耗 100% CPU 的繁重计算期间也是如此,因为 MRI 会定期对线程进行时间切片,以便每个线程轮流运行。

    但是,有一个例外情况是时间片不活动,那就是 Ruby 线程正在执行本机 C 代码而不是 Ruby 代码。

    现在碰巧Array#combination 是用纯 C 实现的:

    [1] pry(main)> show-source Array#combination
    From: array.c (C Method):
    
    static VALUE
    rb_ary_combination(VALUE ary, VALUE num)
    {
      ...
    }
    

    当我们将这些知识与Timeout.timeout 的实现方式结合起来时,我们可以开始了解正在发生的事情:

    [7] pry(main)> show-source Timeout#timeout
    From: /opt/ruby21/lib/ruby/2.1.0/timeout.rb @ line 75:
    
     75: def timeout(sec, klass = nil)   #:yield: +sec+
     76:   return yield(sec) if sec == nil or sec.zero?
     77:   message = "execution expired"
     78:   e = Error
     79:   bl = proc do |exception|
     80:     begin
     81:       x = Thread.current
     82:       y = Thread.start {
     83:         begin
     84:           sleep sec
     85:         rescue => e
     86:           x.raise e
     87:         else
     88:           x.raise exception, message
     89:         end
     90:       }
     91:       return yield(sec)
     92:     ensure
     93:       if y
     94:         y.kill
     95:         y.join # make sure y is dead.
     96:       end
     97:     end
     98:   end
     99:   ...
    1xx: end  
    

    运行Array.combination 的代码很可能实际上开始执行,甚至在超时线程在第84 行运行sleep sec 之前。您的代码从第91 行到yield(sec) 启动。

    这意味着执行顺序实际上变成了:

    1: [thread 1] numbers.combination(5).count 
       # ...some time passes while the combinations are calculated ...
    2: [thread 2] sleep 5 # <- The timeout thread starts running sleep
    3: [thread 1] y.kill  # <- The timeout thread is instantly killed 
                          #    and never times out.
    

    为了确保超时线程先启动,你可以试试这个,这次很可能会触发超时异常:

    Timeout::timeout(5) { Thread.pass; numbers.combination(5).count }
    

    这是因为通过运行 Thread.pass,您允许 MRI 调度程序在本机 combination C 代码执行之前启动并运行第 82 行的代码。然而,即使在这种情况下,也不会因为 GIL 而在 combination 退出之前触发异常。

    不幸的是,没有办法解决这个问题。您将不得不改用 JRuby 之类的东西,它具有真正的并发线程。或者您可以在 Process 而不是线程中运行 combination 计算。

    【讨论】:

      猜你喜欢
      • 2010-10-03
      • 1970-01-01
      • 2014-12-25
      • 2010-09-16
      • 2012-11-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多