Ruby超时模块 - 超时不会执行

时间:2014-12-23 22:22:31

标签: ruby exception timeout

我遇到了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

我对为什么这不起作用感到困惑。

1 个答案:

答案 0 :(得分:2)

问题在于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的东西,它具有真正的并发线程。或者您可以在combination而不是线程中运行Process计算。