Тайм-аут на вызов функции - PullRequest
234 голосов
/ 29 января 2009

Я вызываю функцию в Python, которая, как я знаю, может остановить и заставить меня перезапустить скрипт.

Как мне вызвать функцию или как ее обернуть, чтобы, если это заняло более 5 секунд, сценарий отменил ее и сделал что-то еще?

Ответы [ 14 ]

187 голосов
/ 30 января 2009

Вы можете использовать пакет signal , если вы работаете в UNIX:

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

10 секунд после вызова alarm.alarm(10), вызывается обработчик. Это вызывает исключение, которое вы можете перехватить из обычного кода Python.

Этот модуль плохо работает с потоками (но тогда, кто это делает?)

Обратите внимание, что , поскольку мы вызываем исключение, когда происходит тайм-аут, оно может в конечном итоге перехватиться и проигнорироваться внутри функции, например, одной такой функции:

def loop_forever():
    while 1:
        print 'sec'
        try:
            time.sleep(10)
        except:
            continue
128 голосов
/ 17 февраля 2013

Вы можете использовать multiprocessing.Process, чтобы сделать именно это.

Код

import multiprocessing
import time

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

if __name__ == '__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()
61 голосов
/ 28 июля 2015

Как мне вызвать функцию или как ее обернуть, чтобы, если это заняло более 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

Второй вызов функции не завершится, вместо этого процесс должен завершиться с отслеживанием!

KeyboardInterrupt не всегда останавливает спящий поток

Обратите внимание, что сон не всегда прерывается прерыванием клавиатуры, в Python 2 в Windows, например ::1010 *

@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 игнорируются

Во всяком случае, я бы не спал нить больше секунды - это целая вечность процессора.

Как мне вызвать функцию или как ее обернуть, чтобы, если это займет более 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
44 голосов
/ 11 декабря 2012

У меня есть другое предложение, которое представляет собой чистую функцию (с тем же 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
29 голосов
/ 01 февраля 2016

Я наткнулся на эту тему при поиске тайм-аута в модульных тестах. Я не нашел ничего простого в ответах или сторонних пакетах, поэтому я написал декоратор ниже, вы можете перейти прямо в код:

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):
    ...
14 голосов
/ 03 мая 2017

Есть много предложений, но ни одно из них не использует 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).

Переключение между потоками и процессами так же просто, как замена ProcessPoolExecutor на ThreadPoolExecutor.

Если вы хотите прекратить процесс по тайм-ауту, я бы посоветовал изучить Pebble .

12 голосов
/ 15 февраля 2015

Пакет stopit, найденный на pypi, похоже, хорошо справляется с таймаутами.

Мне нравится декоратор @stopit.threading_timeoutable, который добавляет к декорированной функции параметр timeout, который выполняет то, что вы ожидаете, и останавливает функцию.

Проверьте это на pypi: https://pypi.python.org/pypi/stopit

5 голосов
/ 08 августа 2018

Отличный, простой в использовании и надежный PyPi проект таймаут-декоратор (https://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 __name__ == '__main__':
    mytest()
2 голосов
/ 27 апреля 2016
#!/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)
1 голос
/ 23 апреля 2019

Я являюсь автором wrapt_timeout_decorator

Большинство представленных здесь решений на первый взгляд прекрасно работают под Linux - потому что у нас есть fork () и signal (), но в Windows все выглядит немного иначе. А когда речь идет о подпотоках в Linux, вы больше не можете использовать сигналы.

Чтобы порождать процесс под Windows, его нужно выбирать, а многие декорированные функции или методы класса - нет.

Таким образом, вам нужно использовать лучший инструмент для выбора, такой как укроп и мультипроцесс (не для консервирования и мультипроцессинга) - вот почему вы не можете использовать ProcessPoolExecutor (или только с ограниченной функциональностью).

Для самого тайм-аута - вам нужно определить, что означает тайм-аут - потому что в Windows потребуется значительное (и не определяемое) время, чтобы порождать процесс. Это может быть сложно на коротких таймаутах. Предположим, порождение процесса занимает около 0,5 секунды (легко !!!). Если вы даете тайм-аут 0,2 секунды, что должно произойти? Должна ли функция отключаться через 0,5 + 0,2 секунды (так что метод должен работать в течение 0,2 секунды)? Или время вызова вызываемого процесса истечет через 0,2 секунды (в этом случае декорированная функция ВСЕГДА будет иметь время ожидания, потому что в это время она даже не порождается)?

Также вложенные декораторы могут быть неприятными, и вы не можете использовать сигналы в подпотоке. Если вы хотите создать действительно универсальный, кроссплатформенный декоратор, все это необходимо принять во внимание (и протестировать).

Другие проблемы - передача исключений обратно вызывающей стороне, а также проблемы с журналированием (если используется в оформленной функции - регистрация в файлах в другом процессе НЕ поддерживается)

Я пытался охватить все крайние случаи, вы можете посмотреть на пакет wrapt_timeout_decorator или, по крайней мере, протестировать ваши собственные решения, вдохновленные используемыми там тестами юнитов.

@ Алексис Эггермонт - к сожалению, у меня недостаточно очков, чтобы комментировать, - может быть, кто-то еще может уведомить Вас - я думаю, что я решил проблему с импортом.

...