Pregunta ¿Cuál es la forma correcta de limpiar después de un ciclo de evento interrumpido?


Tengo un bucle de eventos que ejecuta algunas co-rutinas como parte de una herramienta de línea de comandos. El usuario puede interrumpir la herramienta con el habitual Ctrl + do, en cuyo punto quiero limpiar correctamente después del ciclo de eventos interrumpidos.

Esto es lo que intenté.

import asyncio


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


if __name__ == '__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()

Ejecutando esto y golpeando Ctrl + do rendimientos:

$ 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]>

Claramente, no limpié correctamente. Pensé que quizás llamar cancel() en las tareas sería la manera de hacerlo.

¿Cuál es la forma correcta de limpiar después de un ciclo de evento interrumpido?


32
2018-06-10 19:30


origen


Respuestas:


Cuando presionas CTRL + C, el ciclo de eventos se detiene, por lo que tus llamadas a t.cancel() en realidad no tiene efecto. Para que se cancelen las tareas, debe iniciar nuevamente el ciclo.

Así es cómo puedes manejarlo:

import asyncio

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


if __name__ == '__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()

Una vez que atrapamos KeyboardInterrupt, llamamos tasks.cancel() y luego comienza el loop de nuevo run_forever en realidad saldrá tan pronto como tasks se cancela (tenga en cuenta que cancelar el Future devuelto por asyncio.gather también cancela todo el Futures dentro de ella), porque la interrupción loop.run_until_complete llamada agregada una done_callback a tasks eso detiene el ciclo Entonces, cuando cancelamos tasks, esa devolución de llamada se dispara y el ciclo se detiene. En ese momento llamamos tasks.exception, solo para evitar recibir una advertencia acerca de no obtener la excepción del _GatheringFuture.


34
2018-06-10 19:58



Basado en las otras respuestas y algunas ideas, llegué a esta práctica solución que debería funcionar en casi todos los casos de uso y no depende de que usted haga un seguimiento de las tareas que deben limpiarse manualmente. Ctrl+do:

loop = asyncio.get_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:
    loop.close()

El código anterior obtendrá todas las tareas actuales del ciclo de eventos usando asyncio.Task.all_tasks y colocarlos en un solo futuro combinado usando asyncio.gather. Todas las tareas en ese futuro (que son todas las tareas que se están ejecutando actualmente) se cancelan utilizando las futuras .cancel() método. los return_exceptions=True luego se asegura de que todo lo recibido asyncio.CancelledError las excepciones se almacenan en lugar de causar que el futuro tenga errores.

El código anterior anulará el controlador de excepciones predeterminado para evitar


6
2018-02-07 18:40



A menos que esté en Windows, configure manejadores de señal basados ​​en bucle de eventos para SIGINT (y también SIGTERM para que pueda ejecutarlo como un servicio). En estos manejadores, puede salir del bucle de evento inmediatamente o iniciar algún tipo de secuencia de limpieza y salir más tarde.

Ejemplo en la documentación oficial de Python: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and-sigterm


2
2018-06-10 19:38