¿Cuál es la forma correcta de limpiar después de un ciclo de eventos 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 las teclas Ctrl + C habituales, momento en el que quiero limpiar correctamente después del ciclo de eventos interrumpido.

Esto es lo que he intentado.

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 presionando Ctrl + C se obtiene:

 $ 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= wait_for= cb=[gather.._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= wait_for= cb=[gather.._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 tal vez llamar cancel() en las tareas sería la forma de hacerlo.

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

Cuando t.cancel() CTRL + C, el bucle de eventos se detiene, por lo que sus llamadas a t.cancel() realidad no tienen efecto. Para que las tareas se cancelen, debe iniciar de nuevo el bucle.

Aquí 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 tasks.cancel() KeyboardInterrupt , llamamos a tasks.cancel() y luego tasks.cancel() el loop nuevamente. run_forever realmente saldrá tan pronto como las tasks se cancelen (tenga en cuenta que cancelar el Future devuelto por asyncio.gather también cancela todos los Futures dentro de él), debido a que la llamada de loop.run_until_complete interrumpida agregó un done_callback a las tasks que detiene el ciclo. Entonces, cuando cancelamos tasks , esa callback se activa y el bucle se detiene. En ese momento, llamamos a tasks.exception , solo para evitar recibir una advertencia acerca de no obtener la excepción de _GatheringFuture .

Actualizado para Python 3.6+ : agregue una llamada a loop.shutdown_asyncgens para evitar pérdidas de memoria por generadores asíncronos que no se utilizaron por completo. Además, ahora se utiliza asyncio.get_event_loop en lugar de asyncio.get_event_loop para garantizar que la llamada final loop.close no interfiera con otros posibles usos del loop.

La siguiente solución, inspirada en algunas de las otras respuestas, debería funcionar en casi todos los casos y no depende de que usted realice un seguimiento manual de las tareas que deben limpiarse en 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() 

El código anterior obtendrá todas las tareas actuales del bucle de eventos usando asyncio.Task.all_tasks y las colocará en un futuro combinado único usando asyncio.gather . Todas las tareas en ese futuro (que son todas tareas actualmente en ejecución) se cancelan utilizando el método .cancel() del futuro. El return_exceptions=True luego asegura que todas las excepciones asyncio.CancelledError recibidas se almacenan en lugar de causar que el futuro se vuelva erróneo.

El código anterior también anulará el controlador de excepciones predeterminado para evitar que se asyncio.CancelledError excepciones asyncio.CancelledError generadas.

A menos que esté en Windows, configure los manejadores de señales basados ​​en bucles de eventos para SIGINT (y también SIGTERM para que pueda ejecutarlo como un servicio). En estos controladores, puede salir del bucle de eventos 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