Вопрос-Ответ

Timeout on a function call

Тайм-аут при вызове функции

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

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

Переведено автоматически
Ответ 1

Вы можете использовать пакет 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 секунд после вызова signal.alarm(10) вызывается обработчик. При этом возникает исключение, которое можно перехватить из обычного кода Python.

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

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

def loop_forever():
while 1:
print('sec')
try:
time.sleep(10)
except:
continue
Ответ 2

Вы можете использовать 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 - may not work if process is stuck for good
p.terminate()
# OR Kill - will work for sure, no chance for process to finish nicely however
# p.kill()

p.join()
Ответ 3

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

@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 ignored

Я бы в любом случае не стал переводить поток в режим ожидания более секунды - это целая вечность процессорного времени.


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

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