web-dev-qa-db-fra.com

Arrêt progressif des coroutines asyncio

J'ai actuellement des problèmes pour fermer les coroutines asyncio pendant l'arrêt CTRL-C d'une application. Le code suivant est une version allégée de ce que j'ai en ce moment:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import asyncio
import time
import functools
import signal


class DummyProtocol(asyncio.Protocol):

    def __init__(self, *args, **kwargs):
        self._shutdown = asyncio.Event()
        self._response = asyncio.Queue(maxsize=1)
        super().__init__(*args, **kwargs)

    def connection_made(self, transport):
        self.transport = transport

    def close(self):
        print("Closing protocol")
        self._shutdown.set()

    def data_received(self, data):

        #data = b'OK MPD '

        # Start listening for commands after a successful handshake
        if data.startswith(b'OK MPD '):
            print("Ready for sending commands")
            self._proxy_task = asyncio.ensure_future(self._send_commands())
            return

        # saving response for later consumption in self._send_commands
        self._response.put_nowait(data)

    async def _send_commands(self):

        while not self._shutdown.is_set():

            print("Waiting for commands coming in ...")

            command = None

            # listen for commands coming in from the global command queue. Only blocking 1sec.
            try:
                command = await asyncio.wait_for(cmd_queue.get(), timeout=1)
            except asyncio.TimeoutError:
                continue

            # sending the command over the pipe
            self.transport.write(command)

            # waiting for the response. Blocking until response is complete.
            res = await self._response.get()
            # put it into the global response queue
            res_queue.put_nowait(res)


async def connect(loop):
    c = lambda: DummyProtocol()
    t = asyncio.Task(loop.create_connection(c, '192.168.1.143', '6600'))
    try:
        # Wait for 3 seconds, then raise TimeoutError
        trans, proto = await asyncio.wait_for(t, timeout=3)
        print("Connected to <192.168.1.143:6600>.")
        return proto
    except (asyncio.TimeoutError, OSError) as e:
        print("Could not connect to <192.168.1.143:6600>. Trying again ...")
        if isinstance(e, OSError):
            log.exception(e)


def shutdown(proto, loop):
    # http://stackoverflow.com/a/30766124/1230358
    print("Shutdown of DummyProtocol initialized ...")
    proto.close()
    # give the coros time to finish
    time.sleep(2)

    # cancel all other tasks
    # for task in asyncio.Task.all_tasks():
    #    task.cancel()

    # stopping the event loop
    if loop:
        print("Stopping event loop ...")
        loop.stop()

    print("Shutdown complete ...")    


if __name__ == "__main__":

    loop = asyncio.get_event_loop()

    cmd_queue = asyncio.Queue()
    res_queue = asyncio.Queue()

    dummy_proto = loop.run_until_complete(connect(loop))

    for signame in ('SIGINT','SIGTERM'):
        loop.add_signal_handler(getattr(signal, signame), functools.partial(shutdown, dummy_proto, loop))

    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    finally:
        loop.close()

ce qui me donne la sortie suivante si CTRL-C est pressé:

Connected to <192.168.1.143:6600>.
Ready for sending commands
Waiting for commands coming in ...
Waiting for commands coming in ...
Waiting for commands coming in ...
Waiting for commands coming in ...
^CShutdown of DummyProtocol initialized ...
Closing protocol
Stopping event loop ...
Shutdown complete ...
Task was destroyed but it is pending!
task: <Task pending coro=<DummyProtocol._send_commands() running at ./dummy.py:45> wait_for=<Future pending cb=[Task._wakeup()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<Queue.get() running at /usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/queues.py:168> wait_for=<Future pending cb=[Task._wakeup()]> cb=[_release_waiter(<Future pendi...sk._wakeup()]>)() at /usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py:344]>
Exception ignored in: <generator object Queue.get at 0x10594b468>
Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/queues.py", line 170, in get
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/futures.py", line 227, in cancel
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/futures.py", line 242, in _schedule_callbacks
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py", line 447, in call_soon
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py", line 456, in _call_soon
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py", line 284, in _check_closed
RuntimeError: Event loop is closed

Je ne suis pas très expérimenté avec asyncio, donc je suis sûr que je manque quelque chose d'important ici. Ce qui me donne vraiment mal à la tête, c'est la partie de la sortie après Shutdown complete .... Commençant par Task was destroyed but it is pending!, Je dois admettre que je n'ai aucune idée de ce qui se passe. J'ai jeté un œil à d'autres questions, mais je n'ai pas réussi à le faire fonctionner. Alors, pourquoi ce code produit-il des trucs comme Task was destroyed but it is pending! aso.et comment nettoyer proprement les coroutines?

Merci de votre aide!

17
hetsch

Que signifie Task was destroyed but it is pending!?

Si au moment où votre programme a terminé certaines tâches asynchrones qui ne sont toujours pas terminées, vous obtiendrez cet avertissement. Cet avertissement est nécessaire car certaines tâches en cours d'exécution peuvent ne pas libérer correctement certaines ressources.

Il existe deux façons courantes de le résoudre:

  1. Vous pouvez attendre que les tâches se terminent
  2. Vous pouvez annuler des tâches et attendre qu'elles soient terminées

Asyncio et blocage des opérations synchrones

Regardons votre code:

def shutdown(proto, loop):
    print("Shutdown of DummyProtocol initialized ...")
    proto.close()

    time.sleep(2)
    # ...

time.sleep(2) - cette ligne ne donnera pas le temps aux coroutines de terminer. Cela gèlera tout votre programme pendant deux secondes. Rien ne se passera pendant ce temps.

Cela se produit car votre boucle d'événements s'exécute dans le même processus que celui auquel vous appelez time.sleep(2). Vous ne devez jamais appeler des opérations synchrones de longue durée de cette façon dans vos programmes asynchrones. Veuillez lire cette réponse pour voir comment fonctionne le code asynchrone.

Comment pouvons-nous attendre que les tâches soient terminées

Essayons de modifier la fonction shutdown. Ce n'est pas une fonction asynchrone, vous ne pouvez pas await quelque chose à l'intérieur. Pour exécuter du code asynchrone, nous devons le faire manuellement: arrêter la boucle en cours d'exécution (car il est déjà en cours d'exécution), créer une fonction asynchrone pour attendre que les tâches soient terminées, passer cette fonction pour être exécutée dans la boucle d'événements.

def shutdown(proto, loop):
    print("Shutdown of DummyProtocol initialized ...")

    # Set shutdown event: 
    proto.close()

    # Stop loop:
    loop.stop()

    # Find all running tasks:
    pending = asyncio.Task.all_tasks()

    # Run loop until tasks done:
    loop.run_until_complete(asyncio.gather(*pending))

    print("Shutdown complete ...")    

Vous pouvez également annuler les tâches et attendre qu'elles soient terminées. Voir cette réponse pour plus de détails.

Où placer les opérations de nettoyage

Je ne suis pas familier avec les signaux, mais en avez-vous vraiment besoin pour capturer CTRL-C? Chaque fois que KeyboardInterrupt se produit, il sera jeté par ligne, où vous exécutez votre boucle d'événement (dans votre code, c'est loop.run_forever()). Je peux me tromper ici, mais la manière courante de gérer cette situation est de placer toutes les opérations de nettoyage dans le bloc finally.

Par exemple, vous pouvez voir comment aiohttp le fait:

try:
    loop.run_forever()
except KeyboardInterrupt:  # pragma: no branch
    pass
finally:
    srv.close()
    loop.run_until_complete(srv.wait_closed())
    loop.run_until_complete(app.shutdown())
    loop.run_until_complete(handler.finish_connections(shutdown_timeout))
    loop.run_until_complete(app.cleanup())
loop.close()
38