我遇到了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
我对为什么这不起作用感到困惑。
答案 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
计算。