最容易在代码中解释:
require 'timeout'
puts "this block will properly kill the sleep after a second"
IO.popen("sleep 60") do |io|
begin
Timeout.timeout(1) do
while (line=io.gets) do
output += line
end
end
rescue Timeout::Error => ex
Process.kill 9, io.pid
puts "timed out: this block worked correctly"
end
end
puts "but this one blocks for >1 minute"
begin
pid = 0
Timeout.timeout(1) do
IO.popen("sleep 60") do |io|
pid = io.pid
while (line=io.gets) do
output += line
end
end
end
rescue Timeout::Error => ex
puts "timed out: the exception gets thrown, but much too late"
end
我的两个街区的心理模型是相同的:
那么,我错过了什么?
编辑:drmaciver在twitter上建议,在第一种情况下,由于某种原因,管道套接字进入非阻塞模式,但在第二种情况下它没有。我想不出有什么理由会发生这种情况,也无法弄清楚如何获得描述符的标志,但它至少是一个看似合理的答案?研究这种可能性。
答案 0 :(得分:15)
啊哈,微妙。
在第二种情况下,在IO#popen块的末尾有一个隐藏的阻塞ensure
子句。及时引发了Timeout :: Error ,但在执行从隐式rescue
子句返回之前,您不能ensure
。
Under the hood,IO.popen(cmd) { |io| ... }
做了类似的事情:
def my_illustrative_io_popen(cmd, &block)
begin
pio = IO.popen(cmd)
block.call(pio) # This *is* interrupted...
ensure
pio.close # ...but then control goes here, which blocks on cmd's termination
end
并且IO#close call实际上或多或少是pclose(3)
,它会阻止你waitpid(2)
,直到睡着的孩子退出。
您可以这样验证:
#!/usr/bin/env ruby
require 'timeout'
BEGIN { $BASETIME = Time.now.to_i }
def xputs(msg)
puts "%4.2f: %s" % [(Time.now.to_f - $BASETIME), msg]
end
begin
Timeout.timeout(3) do
begin
xputs "popen(sleep 10)"
pio = IO.popen("sleep 10")
sleep 100 # or loop over pio.gets or whatever
ensure
xputs "Entering ensure block"
#Process.kill 9, pio.pid # <--- This would solve your problem!
pio.close
xputs "Leaving ensure block"
end
end
rescue Timeout::Error => ex
xputs "rescuing: #{ex}"
end
那么,你能做什么?
您必须以显式方式执行此操作,因为解释器不会公开覆盖IO#popen ensure
逻辑的方法。您可以将上述代码用作起始模板,并取消注释kill()
行,例如。
答案 1 :(得分:0)
在第一个块中,在子节点中引发超时,将其终止并将控制权返回给父节点。在第二个块中,父节点中会引发超时。孩子永远不会得到信号。
见io.c
https://github.com/ruby/ruby/blob/trunk/io.c#L6021
和timeout.rb
https://github.com/ruby/ruby/blob/trunk/lib/timeout.rb#L51