web-dev-qa-db-fra.com

Quelle est la bonne façon de nettoyer après une boucle d'événement interrompue?

J'ai une boucle d'événements qui exécute certaines co-routines dans le cadre d'un outil de ligne de commande. L'utilisateur peut interrompre l'outil avec la méthode habituelle Ctrl + C, à quel point je veux nettoyer correctement après la boucle d'événement interrompue.

Voici ce que j'ai essayé.

import asyncio


@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


if __== '__main__':
    loop = asyncio.get_event_loop()

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = [
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    ]

    try:
        loop.run_until_complete(asyncio.gather(*tasks))
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")

        # This doesn't seem to be the correct solution.
        for t in tasks:
            t.cancel()
    finally:
        loop.close()

Courir ça et frapper Ctrl + C rendements:

$ python3 asyncio-keyboardinterrupt-example.py 
Shleeping for 5 seconds...
Shleeping for 10 seconds...
^CCaught keyboard interrupt. Canceling tasks...
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(1)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(0)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>

De toute évidence, je n'ai pas nettoyé correctement. Je pensais que peut-être appeler cancel() sur les tâches serait le moyen de le faire.

Quelle est la bonne façon de nettoyer après une boucle d'événement interrompue?

37
Nick Chammas

Lorsque vous CTRL + C, la boucle d'événements est arrêtée, donc vos appels à t.cancel() ne prennent pas réellement effet. Pour que les tâches soient annulées, vous devez recommencer la boucle.

Voici comment vous pouvez le gérer:

import asyncio

@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


if __== '__main__':
    loop = asyncio.get_event_loop()

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = asyncio.gather(
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    )

    try:
        loop.run_until_complete(tasks)
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")
        tasks.cancel()
        loop.run_forever()
        tasks.exception()
    finally:
        loop.close()

Une fois que nous avons attrapé KeyboardInterrupt, nous appelons tasks.cancel() puis redémarrons loop. run_forever Se terminera réellement dès que tasks sera annulé (notez que l'annulation du Future retourné par asyncio.gather Annule également tous les Futures à l'intérieur de celui-ci), car l'appel loop.run_until_complete interrompu a ajouté un done_callback à tasks qui arrête la boucle. Ainsi, lorsque nous annulons tasks, ce rappel se déclenche et la boucle s'arrête. À ce stade, nous appelons tasks.exception, Juste pour éviter de recevoir un avertissement vous invitant à ne pas récupérer l'exception dans _GatheringFuture.

39
dano

Mis à jour pour Python 3.6 +: Ajoutez un appel à loop.shutdown_asyncgens Pour éviter les fuites de mémoire par des générateurs asynchrones qui n'étaient pas entièrement utilisés. De plus asyncio.new_event_loop est maintenant utilisé plutôt que asyncio.get_event_loop pour garantir que l'appel final loop.close n'interfère pas avec d'autres utilisations possibles de la boucle.

La solution suivante, inspirée de certaines des autres réponses, devrait fonctionner dans presque tous les cas et ne dépend pas de votre suivi manuel des tâches à nettoyer. Ctrl+C:

loop = asyncio.new_event_loop()
try:
    # Here `amain(loop)` is the core coroutine that may spawn any
    # number of tasks
    sys.exit(loop.run_until_complete(amain(loop)))
except KeyboardInterrupt:
    # Optionally show a message if the shutdown may take a while
    print("Attempting graceful shutdown, press Ctrl+C again to exit…", flush=True)

    # Do not show `asyncio.CancelledError` exceptions during shutdown
    # (a lot of these may be generated, skip this if you prefer to see them)
    def shutdown_exception_handler(loop, context):
        if "exception" not in context \
        or not isinstance(context["exception"], asyncio.CancelledError):
            loop.default_exception_handler(context)
    loop.set_exception_handler(shutdown_exception_handler)

    # Handle shutdown gracefully by waiting for all tasks to be cancelled
    tasks = asyncio.gather(*asyncio.Task.all_tasks(loop=loop), loop=loop, return_exceptions=True)
    tasks.add_done_callback(lambda t: loop.stop())
    tasks.cancel()

    # Keep the event loop running until it is either destroyed or all
    # tasks have really terminated
    while not tasks.done() and not loop.is_closed():
        loop.run_forever()
finally:
    if hasattr(loop, "shutdown_asyncgens"):  # This check is only needed for Python 3.5 and below
        loop.run_until_complete(loop.shutdown_asyncgens())
    loop.close()

Le code ci-dessus obtiendra toutes les tâches en cours à partir de la boucle d'événement en utilisant asyncio.Task.all_tasks Et les placera dans un futur combiné unique en utilisant asyncio.gather. Toutes les tâches de ce futur (qui sont toutes des tâches en cours d'exécution) sont ensuite annulées à l'aide de la méthode .cancel() du futur. Le return_exceptions=True Garantit alors que toutes les exceptions asyncio.CancelledError Reçues sont stockées au lieu de provoquer une erreur future.

Le code ci-dessus remplacera également le gestionnaire d'exceptions par défaut pour empêcher les exceptions générées asyncio.CancelledError D'être enregistrées.

12
ntninja

À moins que vous ne soyez sous Windows, configurez des gestionnaires de signaux basés sur la boucle d'événements pour SIGINT (et également SIGTERM afin de pouvoir l'exécuter en tant que service). Dans ces gestionnaires, vous pouvez soit quitter la boucle d'événements immédiatement, soit lancer une sorte de séquence de nettoyage et quitter plus tard.

Exemple dans la documentation officielle Python: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and -sigterm

3
Ambroz Bizjak