web-dev-qa-db-ja.com

関数呼び出しのタイムアウト

私はPythonで関数を呼んでいますが、これは停止してスクリプトを再起動するように強制するかもしれません。

5秒を超える時間がかかるとスクリプトがそれを取り消して何か他のことをするように、どのように私はその関数を呼び出すか、または何を私はそれをラップするのですか?

236
Teifion

UNIX上で実行している場合は、 signal パッケージを使用できます。

In [1]: import signal

# Register an handler for the timeout
In [2]: def handler(signum, frame):
   ...:     print "Forever is over!"
   ...:     raise Exception("end of time")
   ...: 

# This function *may* run for an indetermined time...
In [3]: def loop_forever():
   ...:     import time
   ...:     while 1:
   ...:         print "sec"
   ...:         time.sleep(1)
   ...:         
   ...:         

# Register the signal function handler
In [4]: signal.signal(signal.SIGALRM, handler)
Out[4]: 0

# Define a timeout for your function
In [5]: signal.alarm(10)
Out[5]: 0

In [6]: try:
   ...:     loop_forever()
   ...: except Exception, exc: 
   ...:     print exc
   ....: 
sec
sec
sec
sec
sec
sec
sec
sec
Forever is over!
end of time

# Cancel the timer if the function returned before timeout
# (ok, mine won't but yours maybe will :)
In [7]: signal.alarm(0)
Out[7]: 0

alarm.alarm(10)の呼び出しから10秒後に、ハンドラが呼び出されます。これにより、通常のPythonコードから傍受できるという例外が発生します。

このモジュールはスレッドとうまく動作しません(でも、だれがしますか?)

タイムアウトが発生すると例外が発生するので、関数の内部でキャッチして無視されることがあります。たとえば、次のような関数があります。

def loop_forever():
    while 1:
        print 'sec'
        try:
            time.sleep(10)
        except:
            continue
187
piro

multiprocessing.Processを使って正確にそれを行うことができます。

コード

import multiprocessing
import time

# bar
def bar():
    for i in range(100):
        print "Tick"
        time.sleep(1)

if __== '__main__':
    # Start bar as a process
    p = multiprocessing.Process(target=bar)
    p.start()

    # Wait for 10 seconds or until process finishes
    p.join(10)

    # If thread is still active
    if p.is_alive():
        print "running... let's kill it..."

        # Terminate
        p.terminate()
        p.join()
129
ATOzTOA

5秒を超える時間がかかるとスクリプトによってキャンセルされるように、関数を呼び出す方法やラップする方法を教えてください。

私はデコレータとthreading.Timerでこの質問/問題を解決する 要点 を投稿しました。これは内訳付きです。

互換性のためのインポートと設定

Python 2と3でテストされています。Unix/ LinuxとWindowsでも動作します。

まず輸入します。 Pythonのバージョンに関係なくコードの一貫性を保つためのこれらの試み

from __future__ import print_function
import sys
import threading
from time import sleep
try:
    import thread
except ImportError:
    import _thread as thread

バージョンに依存しないコードを使う:

try:
    range, _print = xrange, print
    def print(*args, **kwargs): 
        flush = kwargs.pop('flush', False)
        _print(*args, **kwargs)
        if flush:
            kwargs.get('file', sys.stdout).flush()            
except NameError:
    pass

これで、標準ライブラリから機能をインポートしました。

exit_afterデコレータ

次に、子スレッドからmain()を終了させる関数が必要です。

def quit_function(fn_name):
    # print to stderr, unbuffered in Python 2.
    print('{0} took too long'.format(fn_name), file=sys.stderr)
    sys.stderr.flush() # Python 3 stderr is likely buffered.
    thread.interrupt_main() # raises KeyboardInterrupt

そしてこれがデコレータです。

def exit_after(s):
    '''
    use as decorator to exit process if 
    function takes longer than s seconds
    '''
    def outer(fn):
        def inner(*args, **kwargs):
            timer = threading.Timer(s, quit_function, args=[fn.__name__])
            timer.start()
            try:
                result = fn(*args, **kwargs)
            finally:
                timer.cancel()
            return result
        return inner
    return outer

使用法

そして、これが5秒後に終了することについてのあなたの質問に直接答える使い方です!

@exit_after(5)
def countdown(n):
    print('countdown started', flush=True)
    for i in range(n, -1, -1):
        print(i, end=', ', flush=True)
        sleep(1)
    print('countdown finished')

デモ:

>>> countdown(3)
countdown started
3, 2, 1, 0, countdown finished
>>> countdown(10)
countdown started
10, 9, 8, 7, 6, countdown took too long
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in inner
  File "<stdin>", line 6, in countdown
KeyboardInterrupt

2番目の関数呼び出しは終了せず、代わりにトレースバックでプロセスを終了する必要があります。

KeyboardInterruptは常にスリープ状態のスレッドを停止するわけではありません

Windows上のPython 2では、スリープは常にキーボード割り込みによって中断されるわけではないことに注意してください。

@exit_after(1)
def sleep10():
    sleep(10)
    print('slept 10 seconds')

>>> sleep10()
sleep10 took too long         # Note that it hangs here about 9 more seconds
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in inner
  File "<stdin>", line 3, in sleep10
KeyboardInterrupt

また、明示的にPyErr_CheckSignals()をチェックしない限り、エクステンションで実行されているコードを中断することはないでしょう。 Cython、Python、およびKeyboardInterruptは無視されます

いずれにせよ、私は1秒以上スレッドをスリープ状態にするのを避けたい - それはプロセッサ時間における不都合です。

5秒以上かかる場合はスクリプトがそれをキャンセルし、それ以外の場合はどうするように関数を呼び出すか、またはどのようにラップしますか?

それをキャッチして何か他のことをするには、KeyboardInterruptをキャッチすることができます。

>>> try:
...     countdown(10)
... except KeyboardInterrupt:
...     print('do something else')
... 
countdown started
10, 9, 8, 7, 6, countdown took too long
do something else
61
Aaron Hall

私は(スレッド提案と同じAPIを持つ)純粋な関数であり、(このスレッドに関する提案に基づいて)うまく機能するように思われる異なる提案をしています。

def timeout(func, args=(), kwargs={}, timeout_duration=1, default=None):
    import signal

    class TimeoutError(Exception):
        pass

    def handler(signum, frame):
        raise TimeoutError()

    # set the timeout handler
    signal.signal(signal.SIGALRM, handler) 
    signal.alarm(timeout_duration)
    try:
        result = func(*args, **kwargs)
    except TimeoutError as exc:
        result = default
    finally:
        signal.alarm(0)

    return result
44
Alex

単体テストでタイムアウト呼び出しを検索しているときに、このスレッドに出くわしました。私は答えやサードパーティのパッケージに単純なものは何も見つからなかったので、以下のデコレータをあなたがコードに落とすことができるように書いた:

import multiprocessing.pool
import functools

def timeout(max_timeout):
    """Timeout decorator, parameter in seconds."""
    def timeout_decorator(item):
        """Wrap the original function."""
        @functools.wraps(item)
        def func_wrapper(*args, **kwargs):
            """Closure for function."""
            pool = multiprocessing.pool.ThreadPool(processes=1)
            async_result = pool.apply_async(item, args, kwargs)
            # raises a TimeoutError if execution exceeds max_timeout
            return async_result.get(max_timeout)
        return func_wrapper
    return timeout_decorator

それからテストやあなたが好きな機能をタイムアウトするのはこれと同じくらい簡単です:

@timeout(5.0)  # if execution takes longer than 5 seconds, raise a TimeoutError
def test_base_regression(self):
    ...
29
Rich

多くの提案がありますが、concurrent.futuresを使用する提案はありません。これを処理する最もわかりやすい方法だと思います。

from concurrent.futures import ProcessPoolExecutor

# Warning: this does not terminate function if timeout
def timeout_five(fnc, *args, **kwargs):
    with ProcessPoolExecutor() as p:
        f = p.submit(fnc, *args, **kwargs)
        return f.result(timeout=5)

読みやすく維持しやすい超。

私たちはプールを作り、単一のプロセスをサブミットしてから、最大5秒間待ってから、TimeoutErrorを送出します。

Python 3.2以降にネイティブで2.7にバックポートされました(pip install futures)。

スレッドとプロセスの切り替えは、ProcessPoolExecutorThreadPoolExecutorに置き換えるのと同じくらい簡単です。

あなたがタイムアウトでプロセスを終了したいのなら、私は Pebble を調べることをお勧めします。

14
Brian

Pypiにあるstopitパッケージは、タイムアウトをうまく処理するようです。

私はtimeoutパラメータを装飾された関数に追加する@stopit.threading_timeoutableデコレータが好きです。

Pypiでそれをチェックしてください: https://pypi.python.org/pypi/stopit

12
egeland

偉大で、使いやすく、そして信頼できるPyPiプロジェクトtimeout-decoratorhttps:/ /pypi.org/project/timeout-decorator/

インストール

pip install timeout-decorator

使い方

import time
import timeout_decorator

@timeout_decorator.timeout(5)
def mytest():
    print "Start"
    for i in range(1,10):
        time.sleep(1)
        print "%d seconds have passed" % i

if __== '__main__':
    mytest()
5
Gil
#!/usr/bin/python2
import sys, subprocess, threading
proc = subprocess.Popen(sys.argv[2:])
timer = threading.Timer(float(sys.argv[1]), proc.terminate)
timer.start()
proc.wait()
timer.cancel()
exit(proc.returncode)
2
Hal Canary

私はネイブル時限割り込み(SIGALARMはできない)を必要としていましたが、それはtime.sleep(これはスレッドベースでは妨げられません)アプローチはできません)。私はここからコードをコピーして軽く変更することになりました: http://code.activestate.com/recipes/577600-queue-for-managing-multiple-sigalrm-alarms-concurr/

コード自体:

#!/usr/bin/python

# lightly modified version of http://code.activestate.com/recipes/577600-queue-for-managing-multiple-sigalrm-alarms-concurr/


"""alarm.py: Permits multiple SIGALRM events to be queued.

Uses a `heapq` to store the objects to be called when an alarm signal is
raised, so that the next alarm is always at the top of the heap.
"""

import heapq
import signal
from time import time

__version__ = '$Revision: 2539 $'.split()[1]

alarmlist = []

__new_alarm = lambda t, f, a, k: (t + time(), f, a, k)
__next_alarm = lambda: int(round(alarmlist[0][0] - time())) if alarmlist else None
__set_alarm = lambda: signal.alarm(max(__next_alarm(), 1))


class TimeoutError(Exception):
    def __init__(self, message, id_=None):
        self.message = message
        self.id_ = id_


class Timeout:
    ''' id_ allows for nested timeouts. '''
    def __init__(self, id_=None, seconds=1, error_message='Timeout'):
        self.seconds = seconds
        self.error_message = error_message
        self.id_ = id_
    def handle_timeout(self):
        raise TimeoutError(self.error_message, self.id_)
    def __enter__(self):
        self.this_alarm = alarm(self.seconds, self.handle_timeout)
    def __exit__(self, type, value, traceback):
        try:
            cancel(self.this_alarm) 
        except ValueError:
            pass


def __clear_alarm():
    """Clear an existing alarm.

    If the alarm signal was set to a callable other than our own, queue the
    previous alarm settings.
    """
    oldsec = signal.alarm(0)
    oldfunc = signal.signal(signal.SIGALRM, __alarm_handler)
    if oldsec > 0 and oldfunc != __alarm_handler:
        heapq.heappush(alarmlist, (__new_alarm(oldsec, oldfunc, [], {})))


def __alarm_handler(*zargs):
    """Handle an alarm by calling any due heap entries and resetting the alarm.

    Note that multiple heap entries might get called, especially if calling an
    entry takes a lot of time.
    """
    try:
        nextt = __next_alarm()
        while nextt is not None and nextt <= 0:
            (tm, func, args, keys) = heapq.heappop(alarmlist)
            func(*args, **keys)
            nextt = __next_alarm()
    finally:
        if alarmlist: __set_alarm()


def alarm(sec, func, *args, **keys):
    """Set an alarm.

    When the alarm is raised in `sec` seconds, the handler will call `func`,
    passing `args` and `keys`. Return the heap entry (which is just a big
    Tuple), so that it can be cancelled by calling `cancel()`.
    """
    __clear_alarm()
    try:
        newalarm = __new_alarm(sec, func, args, keys)
        heapq.heappush(alarmlist, newalarm)
        return newalarm
    finally:
        __set_alarm()


def cancel(alarm):
    """Cancel an alarm by passing the heap entry returned by `alarm()`.

    It is an error to try to cancel an alarm which has already occurred.
    """
    __clear_alarm()
    try:
        alarmlist.remove(alarm)
        heapq.heapify(alarmlist)
    finally:
        if alarmlist: __set_alarm()

そして使用例:

import alarm
from time import sleep

try:
    with alarm.Timeout(id_='a', seconds=5):
        try:
            with alarm.Timeout(id_='b', seconds=2):
                sleep(3)
        except alarm.TimeoutError as e:
            print 'raised', e.id_
        sleep(30)
except alarm.TimeoutError as e:
    print 'raised', e.id_
else:
    print 'nope.'
1
James

私はwrapt_timeout_decoratorの作者です

ここに提示された解決策のほとんどは一見したところLinuxの下でうまく機能します - なぜなら私たちはfork()とsignals()を持っているからです - しかしウィンドウでは事情が少し違って見えます。そしてLinux上のサブスレッドに関しては、もはやSignalsを使うことはできません。

Windowsでプロセスを生成するためには、選択可能である必要があります - そして多くの装飾された関数やClassメソッドはそうではありません。

だから、ディルやマルチプロセスのようなより良いpicklerを使う必要があります(pickleやmultiprocessingではありません) - そのため、ProcessPoolExecutorを使うことはできません(あるいは限られた機能でのみ)。

タイムアウト自体について - あなたはタイムアウトが何を意味するかを定義する必要があります - なぜならWindows上でプロセスを生み出すのにかなりの(そして決定不可能な)時間がかかるからです。これは短時間のタイムアウトでは難しいかもしれません。プロセスの生成には約0.5秒かかります(簡単に!!!)。あなたが0.2秒のタイムアウトを与えるとどうなるでしょうか?関数は0.5 + 0.2秒後にタイムアウトする必要があります(そのため、メソッドは0.2秒間実行します)。あるいは、呼び出されたプロセスは0.2秒後にタイムアウトするべきです(その場合、装飾された関数は常にタイムアウトになります、その時間では生成されないためです)。

ネストされたデコレータもまた厄介になることがあり、あなたはサブスレッドでシグナルを使用することはできません。本当に普遍的なクロスプラットフォームのデコレータを作りたいのであれば、これらすべてを考慮に入れる(そしてテストする)必要があります。

他の問題は、ロギングの問題と同様に、呼び出し元に例外を渡すことです(装飾された関数で使用されている場合 - 他のプロセスのファイルへのロギングはサポートされていません)。

私はすべてのEdgeのケースをカバーしようとしました、あなたはパッケージwrapt_timeout_decoratorを調べるか、少なくともそこで使われているユニットテストに触発されたあなた自身の解決策をテストするかもしれません。

@Alexis Eggermont - 残念ながら私はコメントするのに十分なポイントを持っていません - 多分他の誰かがあなたに通知することができます - 私はあなたのインポートの問題を解決したと思います。

1
bitranox

信号にも同じことが使えます。以下の例はあなたに役立つと思います。スレッドに比べて非常に単純です。

import signal

def timeout(signum, frame):
    raise myException

#this is an infinite loop, never ending under normal circumstances
def main():
    print 'Starting Main ',
    while 1:
        print 'in main ',

#SIGALRM is only usable on a unix platform
signal.signal(signal.SIGALRM, timeout)

#change 5 to however many seconds you need
signal.alarm(5)

try:
    main()
except myException:
    print "whoops"
1
A R

timeout-decoratorはWindowsシステムでは動作しません。Windowsはsignalをうまくサポートしていなかったからです。

あなたがWindowsシステムでtimeout-decoratorを使用するならば、あなたは以下を得るでしょう

AttributeError: module 'signal' has no attribute 'SIGALRM'

use_signals=Falseを使うことを提案した人もいましたが、私にはうまくいきませんでした。

作者@bitranoxが次のパッケージを作成しました:

pip install https://github.com/bitranox/wrapt-timeout-decorator/archive/master.Zip

コードサンプル:

import time
from wrapt_timeout_decorator import *

@timeout(5)
def mytest(message):
    print(message)
    for i in range(1,10):
        time.sleep(1)
        print('{} seconds have passed'.format(i))

def main():
    mytest('starting')


if __== '__main__':
    main()

次の例外があります。

TimeoutError: Function mytest timed out after 5 seconds
1
as - if

これは与えられたスレッドベースの解決策に対するわずかな改善です。

以下のコードは例外をサポートします。

def runFunctionCatchExceptions(func, *args, **kwargs):
    try:
        result = func(*args, **kwargs)
    except Exception, message:
        return ["exception", message]

    return ["RESULT", result]


def runFunctionWithTimeout(func, args=(), kwargs={}, timeout_duration=10, default=None):
    import threading
    class InterruptableThread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
            self.result = default
        def run(self):
            self.result = runFunctionCatchExceptions(func, *args, **kwargs)
    it = InterruptableThread()
    it.start()
    it.join(timeout_duration)
    if it.isAlive():
        return default

    if it.result[0] == "exception":
        raise it.result[1]

    return it.result[1]

5秒のタイムアウトでそれを呼び出す:

result = timeout(remote_calculate, (myarg,), timeout_duration=5)
0
diemacht