Ruby 2.4.0 リファレンスマニュアル > ライブラリ一覧 > timeoutライブラリ > Timeoutモジュール > timeout

module function Timeout.#timeout

timeout(sec, exception_class = nil) {|i| ... } -> object[permalink][rdoc]
timeout(sec, exception_class, message) {|i| ... } -> object

ブロックを sec 秒の期限付きで実行します。 ブロックの実行時間が制限を過ぎたときは例外 Timeout::Error が発生します。

exception_class を指定した場合には Timeout::Error の代わりに その例外が発生します。 ブロックパラメータ i は sec がはいります。

また sec が nil のときは制限時間なしで ブロックを実行します。

[PARAM] sec:
タイムアウトする時間を秒数で指定します.
[PARAM] exception_class:
タイムアウトした時、発生させる例外を指定します.
[PARAM] message:
エラーメッセージを指定します.省略した場合は "execution expired" になります.

例 長い計算のタイムアウト

require 'timeout'

def calc_pi(min)
  loop do
    x = rand
    y = rand
    x**2 + y**2 < 1.0 ?  min[0] += 1 : min[1] += 1
  end
end

t = 5
min = [ 0, 0]
begin
  timeout(t){
    calc_pi(min)
  }
rescue Timeout::Error
  puts "timeout"
end

printf "%d: pi = %f\n", min[0] + min[1], min[0]*4.0/(min[0]+min[1])
#例
#=> 417519: pi = 3.141443

例 独自の例外を発生させるタイムアウト

#!/usr/bin/env ruby

require 'timeout'

class MYError < Exception;end
begin
  timeout(5, MYError) {
    sleep(30)
  }
rescue MYError => err
  puts "MYError"
  puts err
end

注意

timeout による割り込みは Thread によって実現されています。 C 言語レベルで実装され、 Ruby のスレッドが割り込めない処理に対して timeout は無力です。 そのようなものは実用レベルでは少ないのですが、 Socket などは DNSの名前解決に時間がかかった場合割り込めません (resolv-replace を使用する必要があります)。 その処理を Ruby で実装しなおすか C 側で Ruby のスレッドを意識してあげる必要があります。

以下の例では、gethostbyname(およそ0.6秒処理に時間がかかっている) が終了した直後((A)の箇所)で TimeoutError 例外があがっています。

例 timeout が割り込めない

require 'timeout'
require 'socket'

t = 0.1
start = Time.now
begin
  timeout(t) {
    p TCPSocket.gethostbyname("www.ruby-lang.org")
    # (A)
  }
ensure
  p Time.now - start
end
# 実行例
=> ["helium.ruby-lang.org", [], 2, "210.251.121.214"]
   0.689331
   /usr/local/lib/ruby/1.6/timeout.rb:37: execution expired (TimeoutError)
         from -:6:in `timeout'
         from -:6
# gethostbyname が0.1秒かからない場合は例外が発生しないので
# その場合は、t に小さい数値(0.000001のような)に変える。

timeout による割り込みは Kernel.#system によって呼び出された外部プログラムを タイムアウトさせる事はできないので、IO.popenKernel.#openを使用するなどの工夫が必要です。

例 外部コマンドのタイムアウト

require 'timeout'

# テスト用のシェルをつくる。
File.open("loop.sh", "w"){|fp|
  fp.print <<SHELL_EOT
#!/bin/bash

S="scale=10"
M=32767

trap 'echo "$S; $m1/($m1+$m2)*4" | bc ; echo "count = $((m1+m2))" ; exit 0' INT
m1=0
m2=0

while true
do
  x="($RANDOM/$M)"
  y="($RANDOM/$M)"
  c=$(echo "$S;$x^2+$y^2 < 1.0" | bc)
  echo $x $y $c
  if [ $c -eq 1 ]
  then
    let m1++
  else
    let m2++
  fi
done
SHELL_EOT
}

File.chmod(0755, "loop.sh")
t = 10 # 10 秒でタイムアウト
begin
  pid = nil
  com = nil
  timeout(t) {
    # system だととまらない
    # system("./loop.sh")
    com = IO.popen("./loop.sh")
    pid = com.pid
    while line = com.gets
      print line
    end
  }
rescue Timeout::Error => err
  puts "timeout: shell execution."
  Process.kill('SIGINT', pid)
  printf "[result]\t%s", com.read
  com.close unless com.nil?
end

#止まっているか確認する。
#system("ps au")