web-dev-qa-db-fra.com

casser la fonction après un certain temps

En python, pour un exemple de jouet:

for x in range(0, 3):
    # call function A(x)

Je souhaite continuer la boucle for si la fonction A prend plus de 5 secondes en la sautant pour ne pas me coincer ou perdre du temps. 

En faisant une recherche, j'ai réalisé que le sous-processus ou le fil pouvait aider, mais je ne sais pas comment le mettre en œuvre ici . Toute aide sera utile . Merci

17
user2372074

Je pense que créer un nouveau processus peut être exagéré. Si vous utilisez un système Mac ou Unix, vous devriez pouvoir utiliser signal.SIGALRM pour forcer l'expiration des fonctions trop longues. Cela fonctionnera avec des fonctions inactives pour des problèmes de réseau ou d'autres problèmes que vous ne pouvez absolument pas gérer en modifiant votre fonction. J'ai un exemple d'utilisation dans cette réponse:

https://stackoverflow.com/a/24921763/3803152

Je modifie ma réponse ici, bien que je ne sois pas sûr de pouvoir le faire:

import signal

class TimeoutException(Exception):   # Custom exception class
    pass

def timeout_handler(signum, frame):   # Custom signal handler
    raise TimeoutException

# Change the behavior of SIGALRM
signal.signal(signal.SIGALRM, timeout_handler)

for i in range(3):
    # Start the timer. Once 5 seconds are over, a SIGALRM signal is sent.
    signal.alarm(5)    
    # This try/except loop ensures that 
    #   you'll catch TimeoutException when it's sent.
    try:
        A(i) # Whatever your function that might hang
    except TimeoutException:
        continue # continue the for loop if function A takes more than 5 second
    else:
        # Reset the alarm
        signal.alarm(0)

Cela définit essentiellement une minuterie pendant 5 secondes, puis tente d'exécuter votre code. Si elle ne se termine pas avant la fin du temps imparti, un SIGALRM est envoyé, que nous capturons et transformons en une exception TimeoutException. Cela vous oblige au bloc sauf, où votre programme peut continuer.

EDIT: whoops, TimeoutException est une classe, pas une fonction. Merci, abarnert!

29
TheSoundDefense

Les commentaires sont corrects en ce que vous devriez vérifier à l'intérieur. Voici une solution potentielle. Notez qu'une fonction asynchrone (en utilisant un thread par exemple) est différente de cette solution. C'est synchrone, ce qui signifie qu'il fonctionnera toujours en série.

import time

for x in range(0,3):
    someFunction()

def someFunction():
    start = time.time()
    while (time.time() - start < 5):
        # do your normal function

    return;
7
ZekeDroid

Si vous pouvez interrompre votre travail et vérifier de temps en temps, c'est presque toujours la meilleure solution. Mais parfois, cela n’est pas possible - par exemple, vous lisez peut-être un fichier sur un partage de fichiers lent qui, de temps à autre, reste suspendu pendant 30 secondes. Pour gérer cela en interne, vous devez restructurer tout votre programme autour d'une boucle asynchrone d'E/S.

Si vous n'avez pas besoin d'être multiplate-forme, vous pouvez utiliser des signaux sur * nix (y compris Mac et Linux), des APC sous Windows, etc. Mais si vous devez être multiplate-forme, cela ne fonctionne pas.

Donc, si vous avez vraiment besoin de le faire simultanément, vous pouvez le faire, et parfois vous devez le faire. Dans ce cas, vous souhaiterez probablement utiliser un processus pour cela, pas un thread. Vous ne pouvez pas vraiment tuer un thread en toute sécurité, mais vous pouvez tuer un processus et il peut être aussi sûr que vous le souhaitez. De plus, si le thread prend plus de 5 secondes parce qu'il est lié au processeur, vous ne voulez pas vous battre avec le GIL.

Il y a deux options de base ici.


Tout d'abord, vous pouvez insérer le code dans un autre script et l'exécuter avec subprocess:

subprocess.check_call([sys.executable, 'other_script.py', arg, other_arg],
                      timeout=5)

Comme cela passe par des canaux de processus enfants normaux, la seule communication que vous pouvez utiliser est une chaîne argv, une valeur de retour succès/échec (en réalité un petit entier, mais ce n'est pas beaucoup mieux), et éventuellement un bloc de texte qui entre et un morceau de texte qui sort.


Vous pouvez également utiliser multiprocessing pour générer un processus enfant de type thread:

p = multiprocessing.Process(func, args)
p.start()
p.join(5)
if p.is_alive():
    p.terminate()

Comme vous pouvez le constater, c'est un peu plus compliqué, mais c'est mieux à quelques égards:

  • Vous pouvez passer des objets Python arbitraires (au moins tout ce qui peut être décapé) plutôt que de simples chaînes.
  • Au lieu d'avoir à placer le code cible dans un script complètement indépendant, vous pouvez le laisser comme fonction dans le même script.
  • C'est plus flexible. Par exemple, si vous devez par la suite transmettre des mises à jour de progression, il est très facile d'ajouter une file d'attente dans un sens ou dans l'autre.

Le gros problème de tout type de parallélisme est le partage de données modifiables, par exemple, le fait de mettre à jour un dictionnaire global dans le cadre de son travail (ce que vos commentaires disent que vous essayez de faire). Avec les threads, vous pouvez en quelque sorte vous en sortir, mais les conditions de concurrence peuvent conduire à des données corrompues, vous devez donc être très prudent avec le verrouillage. Avec les processus enfants, vous ne pouvez pas vous en sortir. (Oui, vous pouvez utiliser la mémoire partagée en tant que Etat de partage entre processus explique, mais cela se limite à des types simples tels que des nombres, des tableaux fixes et des types que vous savez définir en tant que structures C, et cela vous ramène à la vie. aux mêmes problèmes que les discussions.)


Idéalement, vous organisez les choses de sorte que vous n’ayez pas besoin de partager de données pendant l’exécution du processus; vous transmettez une valeur dict en tant que paramètre et vous obtenez une valeur dict en retour. Cela est généralement assez facile à organiser lorsque vous avez une fonction précédemment synchrone que vous souhaitez mettre en arrière-plan. 

Mais que se passe-t-il si, par exemple, un résultat partiel vaut mieux que pas de résultat? Dans ce cas, la solution la plus simple consiste à transmettre les résultats sur une file d'attente. Vous pouvez le faire avec une file d'attente explicite, comme expliqué dans Echange d'objets entre processus , mais il existe un moyen plus simple.

Si vous pouvez diviser le processus monolithique en tâches distinctes, une pour chaque valeur (ou groupe de valeurs) que vous souhaitez conserver dans le dictionnaire, vous pouvez les planifier sur une Pool— ou, mieux encore, un concurrent.futures.Executor . (Si vous utilisez Python 2.x ou 3.1, consultez le rapport arrière futures sur PyPI.)

Disons que votre fonction lente ressemble à ceci:

def spam():
    global d
    for meat in get_all_meats():
        count = get_meat_count(meat)
        d.setdefault(meat, 0) += count

Au lieu de cela, vous feriez ceci:

def spam_one(meat):
    count = get_meat_count(meat)
    return meat, count

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    results = executor.map(spam_one, get_canned_meats(), timeout=5)
    for (meat, count) in results:
        d.setdefault(meat, 0) += count

Tous les résultats obtenus en moins de 5 secondes sont ajoutés au dict; si ce n'est pas tout, le reste est abandonné et une TimeoutError est levée (que vous pouvez gérer comme vous le souhaitez - enregistrez-le, créez un code de secours rapide, peu importe).

Et si les tâches sont vraiment indépendantes (comme dans mon stupide petit exemple, mais bien sûr, elles peuvent ne pas être dans votre vrai code, du moins sans une refonte majeure), vous pouvez paralléliser le travail gratuitement en supprimant simplement ce max_workers=1 . Ensuite, si vous l'exécutez sur une machine à 8 cœurs, il lancera 8 ouvriers et leur donnera chaque 1/8 du travail à faire, et les choses se feront plus rapidement. (Habituellement pas 8 fois aussi vite, mais souvent 3 à 6 fois aussi vite, ce qui est quand même assez joli.)

7
abarnert

Cela semble être une meilleure idée (désolé, je ne suis pas encore sûr du nom de la chose en python):

import signal

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

signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(3)   # Three seconds
try:
    for x in range(0, 3):
        # call function A(x)
except Exception, msg:
    print "Timeout!"
signal.alarm(0)    # reset
1
Mikel Pascual

Peut-être que quelqu'un trouvera ce décorateur utile, basé sur la réponse de TheSoundDefense:

import time
import signal

class TimeoutException(Exception):   # Custom exception class
pass


def break_after(seconds=2):
    def timeout_handler(signum, frame):   # Custom signal handler
        raise TimeoutException
    def function(function):
        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(seconds)
            try:
                res = function(*args, **kwargs)
                signal.alarm(0)      # Clear alarm
                return res
            except TimeoutException:
                print u'Oops, timeout: %s sec reached.' % seconds, function.__name__, args, kwargs
            return
        return wrapper
    return function

tester:

@break_after(3)
def test(a,b,c):
    return time.sleep(10)

>>> test(1,2,3)
Oops, timeout: 3 sec reached. test (1, 2, 3) {}
1
Igor Kremin