Правильный способ запустить код с тайм-аутом в Python

Я посмотрел онлайн и нашел несколько SO и обсуждающих ActiveState рецептов для запуска некоторого кода с таймаутом. Похоже, есть несколько общих подходов:

  • Используйте поток, который запускает код, и join это с таймаутом. Если истекло время ожидания - убить поток. Это не поддерживается напрямую в Python (используется приватно _Thread__stop функция) так что это плохая практика
  • использование signal.SIGALRM - но этот подход не работает на Windows!
  • Используйте подпроцесс с тайм-аутом - но это слишком тяжело - что если я хочу часто запускать прерываемую задачу, я не хочу запускать процесс для каждого!

Итак, каков правильный путь? Я не спрашиваю об обходных путях (например, используйте Twisted и async IO), но о реальном способе решения актуальной проблемы - у меня есть какая-то функция, и я хочу запустить ее только с некоторым таймаутом. Если тайм-аут истек, я хочу вернуть контроль. И я хочу, чтобы он работал на Linux и Windows.

10 ответов

Решение

Полностью общего решения этого действительно, честно говоря, не существует. Вы должны использовать правильное решение для данного домена.

  • Если вы хотите тайм-ауты для кода, который вы полностью контролируете, вы должны написать его, чтобы сотрудничать. Такой код должен каким-то образом разбиваться на маленькие кусочки, как в системе, управляемой событиями. Вы также можете сделать это с помощью многопоточности, если можете быть уверены, что ничто не удержит блокировку слишком долго, но правильно обрабатывать блокировки довольно сложно.

  • Если вы хотите тайм-ауты, потому что боитесь, что код вышел из-под контроля (например, если вы боитесь, что пользователь попросит ваш калькулятор вычислить 9**(9**9)), вам нужно запустить его в другом процессе. Это единственный простой способ достаточно изолировать его. Запустить его в вашей системе событий или даже в другом потоке будет недостаточно. Также возможно разбить вещи на маленькие кусочки, похожие на другое решение, но требует очень осторожного обращения и, как правило, того не стоит; в любом случае, это не позволяет вам делать то же самое, что и просто запуск кода Python.

То, что вы можете искать, это многопроцессорный модуль. Если subprocess слишком тяжелый, то это может не соответствовать вашим потребностям.

import time
import multiprocessing

def do_this_other_thing_that_may_take_too_long(duration):
    time.sleep(duration)
    return 'done after sleeping {0} seconds.'.format(duration)

pool = multiprocessing.Pool(1)
print 'starting....'
res = pool.apply_async(do_this_other_thing_that_may_take_too_long, [8])
for timeout in range(1, 10):
    try:
        print '{0}: {1}'.format(duration, res.get(timeout))
    except multiprocessing.TimeoutError:
        print '{0}: timed out'.format(duration) 

print 'end'

Если это связано с сетью, вы можете попробовать:

import socket
socket.setdefaulttimeout(number)

Я нашел это с библиотекой eventlet:

http://eventlet.net/doc/modules/timeout.html

from eventlet.timeout import Timeout

timeout = Timeout(seconds, exception)
try:
    ... # execution here is limited by timeout
finally:
    timeout.cancel()

Другой способ - использовать обработчик ошибок:

import time
import faulthandler


faulthandler.enable()


try:
    faulthandler.dump_tracebacks_later(3)
    time.sleep(10)
finally:
    faulthandler.cancel_dump_tracebacks_later()

NB: модуль обработчика ошибок является частью stdlib в python3.3.

Для "нормального" кода Python, который не задерживается в расширениях C или ожиданиях ввода-вывода, вы можете достичь своей цели, установив функцию трассировки с помощью sys.settrace() который прерывает работающий код по истечении времени ожидания.

Достаточно ли этого или нет, зависит от того, насколько вы работаете с вредоносным кодом. Если он хорошо себя ведет, функции отслеживания достаточно.

Я решил это таким образом: для меня это отлично работает (в Windows и не тяжелый вообще) Я надеюсь, что это было полезно для кого-то)

import threading
import time

class LongFunctionInside(object):
    lock_state = threading.Lock()
    working = False

    def long_function(self, timeout):

        self.working = True

        timeout_work = threading.Thread(name="thread_name", target=self.work_time, args=(timeout,))
        timeout_work.setDaemon(True)
        timeout_work.start()

        while True:  # endless/long work
            time.sleep(0.1)  # in this rate the CPU is almost not used
            if not self.working:  # if state is working == true still working
                break
        self.set_state(True)

    def work_time(self, sleep_time):  # thread function that just sleeping specified time,
    # in wake up it asking if function still working if it does set the secured variable work to false
        time.sleep(sleep_time)
        if self.working:
            self.set_state(False)

    def set_state(self, state):  # secured state change
        while True:
            self.lock_state.acquire()
            try:
                self.working = state
                break
            finally:
                self.lock_state.release()

lw = LongFunctionInside()
lw.long_function(10)

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

Решение с конструкцией "с" и решение слияния из -

  • Функция тайм-аута, если она занимает слишком много времени, чтобы закончить
  • эта нить, которая работает лучше.

    import threading, time
    
    class Exception_TIMEOUT(Exception):
        pass
    
    class linwintimeout:
    
        def __init__(self, f, seconds=1.0, error_message='Timeout'):
            self.seconds = seconds
            self.thread = threading.Thread(target=f)
            self.thread.daemon = True
            self.error_message = error_message
    
        def handle_timeout(self):
            raise Exception_TIMEOUT(self.error_message)
    
        def __enter__(self):
            try:
                self.thread.start()
                self.thread.join(self.seconds)
            except Exception, te:
                raise te
    
        def __exit__(self, type, value, traceback):
            if self.thread.is_alive():
                return self.handle_timeout()
    
    def function():
        while True:
            print "keep printing ...", time.sleep(1)
    
    try:
        with linwintimeout(function, seconds=5.0, error_message='exceeded timeout of %s seconds' % 5.0):
            pass
    except Exception_TIMEOUT, e:
        print "  attention !! execeeded timeout, giving up ... %s " % e
    

Здесь заслуживает упоминания библиотека wrapt -timeout-decorator:

      from wrapt_timeout_decorator import timeout

@timeout(10)
def function_with_timeout():
   ...

Если функция не завершается в течение 10 секунд, выдается ошибка.

Если вы запускаете код, который ожидаете смерти через определенное время, вы должны написать его правильно, чтобы не было никаких негативных последствий при завершении работы, независимо от того, является ли он потоком или подпроцессом. Шаблон команды с отменой будет полезен здесь.

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

Что поддерживается в Python, когда дело доходит до потоков? Демон темы и соединения. Почему python позволяет основному потоку завершиться, если вы присоединились к демону, пока он еще активен? Потому что понятно, что кто-то, использующий потоки демона, (будем надеяться) напишет код так, чтобы это не имело значения, когда этот поток умирает. В этом контексте совершенно приемлемо дать тайм-аут для объединения, а затем дать основному объекту умереть и, таким образом, взять с собой все потоки демонов.

Другие вопросы по тегам