web-dev-qa-db-fra.com

Comment limiter le temps d'exécution d'un appel de fonction dans Python

Il y a un appel de fonction lié à la socket dans mon code, cette fonction est d'un autre module donc hors de mon contrôle, le problème est qu'il bloque parfois pendant des heures, ce qui est totalement inacceptable, comment puis-je limiter le temps d'exécution de la fonction à partir de mon code? Je suppose que la solution doit utiliser un autre fil.

63
btw0

Je ne sais pas comment cela pourrait être multiplateforme, mais l'utilisation de signaux et d'alarme pourrait être une bonne façon de voir les choses. Avec un peu de travail, vous pourriez également rendre ce produit complètement générique et utilisable dans n'importe quelle situation.

http://docs.python.org/library/signal.html

Donc, votre code va ressembler à ceci.

import signal

def signal_handler(signum, frame):
    raise Exception("Timed out!")

signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(10)   # Ten seconds
try:
    long_function_call()
except Exception, msg:
    print "Timed out!"
37
rik.the.vik

Une amélioration de la réponse de @ rik.the.vik serait d'utiliser la commande with pour donner à la fonction timeout du sucre syntaxique:

import signal
from contextlib import contextmanager

class TimeoutException(Exception): pass

@contextmanager
def time_limit(seconds):
    def signal_handler(signum, frame):
        raise TimeoutException("Timed out!")
    signal.signal(signal.SIGALRM, signal_handler)
    signal.alarm(seconds)
    try:
        yield
    finally:
        signal.alarm(0)


try:
    with time_limit(10):
        long_function_call()
except TimeoutException as e:
    print("Timed out!")
80
Josh Lee

Voici une façon Linux/OSX de limiter le temps d'exécution d'une fonction. C'est au cas où vous ne souhaitez pas utiliser de threads et que votre programme attend la fin de la fonction ou l'expiration du délai.

from multiprocessing import Process
from time import sleep

def f(time):
    sleep(time)


def run_with_limited_time(func, args, kwargs, time):
    """Runs a function with time limit

    :param func: The function to run
    :param args: The functions args, given as Tuple
    :param kwargs: The functions keywords, given as dict
    :param time: The time limit in seconds
    :return: True if the function ended successfully. False if it was terminated.
    """
    p = Process(target=func, args=args, kwargs=kwargs)
    p.start()
    p.join(time)
    if p.is_alive():
        p.terminate()
        return False

    return True


if __== '__main__':
    print run_with_limited_time(f, (1.5, ), {}, 2.5) # True
    print run_with_limited_time(f, (3.5, ), {}, 2.5) # False
17
Ariel Cabib

Je préfère une approche de gestionnaire de contexte car elle permet l'exécution de plusieurs instructions python dans une instruction with time_limit. Parce que le système Windows n'a pas SIGALARM, un plus portable et une méthode peut-être plus simple pourrait être d'utiliser un Timer

from contextlib import contextmanager
import threading
import _thread

class TimeoutException(Exception):
    def __init__(self, msg=''):
        self.msg = msg

@contextmanager
def time_limit(seconds, msg=''):
    timer = threading.Timer(seconds, lambda: _thread.interrupt_main())
    timer.start()
    try:
        yield
    except KeyboardInterrupt:
        raise TimeoutException("Timed out for operation {}".format(msg))
    finally:
        # if the action ends in specified time, timer is canceled
        timer.cancel()

import time
# ends after 5 seconds
with time_limit(5, 'sleep'):
    for i in range(10):
        time.sleep(1)

# this will actually end after 10 seconds
with time_limit(5, 'sleep'):
    time.sleep(10)

La technique clé ici est l'utilisation de _thread.interrupt_main Pour interrompre le thread principal du thread timer. Une mise en garde est que le thread principal ne répond pas toujours rapidement au KeyboardInterrupt soulevé par le Timer. Par exemple, time.sleep() appelle une fonction système afin qu'un KeyboardInterrupt soit traité après l'appel sleep.

12
user2283347

Faire cela à partir d'un gestionnaire de signaux est dangereux: vous pourriez être à l'intérieur d'un gestionnaire d'exceptions au moment où l'exception est levée et laisser les choses dans un état cassé. Par exemple,

def function_with_enforced_timeout():
  f = open_temporary_file()
  try:
   ...
  finally:
   here()
   unlink(f.filename)

Si votre exception est levée ici (), le fichier temporaire ne sera jamais supprimé.

La solution ici consiste à reporter les exceptions asynchrones jusqu'à ce que le code ne se trouve pas dans le code de gestion des exceptions (un bloc excepté ou finalement), mais Python ne fait pas cela.

Notez que cela n'interrompt rien pendant l'exécution de code natif; cela ne l'interrompra que lorsque la fonction reviendra, donc cela peut ne pas aider ce cas particulier. (SIGALRM lui-même peut interrompre l'appel bloquant - mais le code de socket réessaye généralement simplement après un EINTR.)

Faire cela avec des threads est une meilleure idée, car il est plus portable que les signaux. Puisque vous démarrez un thread de travail et le bloquez jusqu'à ce qu'il se termine, il n'y a pas de soucis de concurrence habituels. Malheureusement, il n'y a aucun moyen de délivrer une exception de manière asynchrone à un autre thread dans Python (d'autres API de thread peuvent le faire). Il y aura également le même problème avec l'envoi d'une exception lors d'un gestionnaire d'exceptions, et nécessitent le même correctif.

7
Glenn Maynard

Vous n'êtes pas obligé d'utiliser des threads. Vous pouvez utiliser un autre processus pour effectuer le travail de blocage, par exemple, en utilisant peut-être le module sous-processus . Si vous souhaitez partager des structures de données entre différentes parties de votre programme, alors Twisted est une excellente bibliothèque pour vous donner le contrôle de cela, et je le recommanderais si vous vous souciez du blocage et prévoyez d'avoir ce problème beaucoup. La mauvaise nouvelle avec Twisted est que vous devez réécrire votre code pour éviter tout blocage, et il y a une courbe d'apprentissage équitable.

Vous pouvez utiliser des threads pour éviter le blocage, mais je considérerais cela comme un dernier recours, car cela vous expose à tout un monde de douleur. Lisez un bon livre sur la concurrence avant même de penser à utiliser des threads dans la production, par exemple "Concurrent Systems" de Jean Bacon. Je travaille avec un tas de gens qui font vraiment des trucs très performants avec des threads, et nous n'introduisons pas de threads dans les projets à moins d'en avoir vraiment besoin.

5
Dickon Reed

La seule façon "sûre" de le faire, dans n'importe quelle langue, est d'utiliser un processus secondaire pour faire cette temporisation, sinon vous devez construire votre code de manière à ce qu'il expire en toute sécurité par lui-même, par exemple en vérifier le temps écoulé dans une boucle ou similaire. Si changer la méthode n'est pas une option, un thread ne suffira pas.

Pourquoi? Parce que vous risquez de laisser les choses en mauvais état lorsque vous le faites. Si le thread est simplement tué à mi-chemin, les verrous maintenus, etc. seront simplement maintenus et ne pourront pas être libérés.

Regardez donc le chemin du processus, faites pas regardez le chemin du fil.

Voici une fonction de délai d'attente que je pense avoir trouvée via google et cela fonctionne pour moi.

De: http://code.activestate.com/recipes/473878/

def timeout(func, args=(), kwargs={}, timeout_duration=1, default=None):
    '''This function will spwan a thread and run the given function using the args, kwargs and 
    return the given default value if the timeout_duration is exceeded 
    ''' 
    import threading
    class InterruptableThread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
            self.result = default
        def run(self):
            try:
                self.result = func(*args, **kwargs)
            except:
                self.result = default
    it = InterruptableThread()
    it.start()
    it.join(timeout_duration)
    if it.isAlive():
        return it.result
    else:
        return it.result   
1
monkut

Je préfère généralement utiliser un gestionnaire de contexte comme suggéré par @ josh-lee

Mais au cas où quelqu'un souhaiterait que cela soit implémenté comme décorateur, voici une alternative.

Voici à quoi cela ressemblerait:

import time
from timeout import timeout

class Test(object):
    @timeout(2)
    def test_a(self, foo, bar):
        print foo
        time.sleep(1)
        print bar
        return 'A Done'

    @timeout(2)
    def test_b(self, foo, bar):
        print foo
        time.sleep(3)
        print bar
        return 'B Done'

t = Test()
print t.test_a('python', 'rocks')
print t.test_b('timing', 'out')

Et c'est le timeout.py module:

import threading

class TimeoutError(Exception):
    pass

class InterruptableThread(threading.Thread):
    def __init__(self, func, *args, **kwargs):
        threading.Thread.__init__(self)
        self._func = func
        self._args = args
        self._kwargs = kwargs
        self._result = None

    def run(self):
        self._result = self._func(*self._args, **self._kwargs)

    @property
    def result(self):
        return self._result


class timeout(object):
    def __init__(self, sec):
        self._sec = sec

    def __call__(self, f):
        def wrapped_f(*args, **kwargs):
            it = InterruptableThread(f, *args, **kwargs)
            it.start()
            it.join(self._sec)
            if not it.is_alive():
                return it.result
            raise TimeoutError('execution expired')
        return wrapped_f

Le résultat:

python
rocks
A Done
timing
Traceback (most recent call last):
  ...
timeout.TimeoutError: execution expired
out

Notez que même si le TimeoutError est levé, la méthode décorée continuera à s'exécuter dans un thread différent. Si vous souhaitez également que ce thread soit "arrêté", voyez: Existe-t-il un moyen de tuer un thread en Python?

1
Seba