python python-3.4 python-asyncio

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



python-3.4 python-asyncio (3)

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 la Ctrl + C habitual, 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()

Ejecutar esto y presionar Ctrl + C produce:

$ 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 a cancel() en las tareas sería la forma de hacerlo.

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



Cuando t.cancel() CTRL + C, el ciclo de eventos se detiene, por lo que sus llamadas a t.cancel() no tienen 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 tasks.cancel() KeyboardInterrupt , llamamos a tasks.cancel() y luego tasks.cancel() nuevamente el loop . run_forever saldrá realmente tan pronto como se cancelen las tasks (tenga en cuenta que cancelar el Future devuelto por asyncio.gather también cancela todos los Futures dentro de él), porque la llamada loop.run_until_complete interrumpida agregó un done_callback a las tasks que detienen el ciclo. Entonces, cuando cancelamos tasks , esa devolución de llamada se dispara, y el ciclo se detiene. En ese punto llamamos tasks.exception , solo para evitar recibir una advertencia acerca de no obtener la excepción de _GatheringFuture .


En base a 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 con Ctrl + C :

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 utilizando asyncio.Task.all_tasks y las colocará en un solo futuro combinado utilizando asyncio.gather . Todas las tareas en ese futuro (que son todas las tareas que se están ejecutando actualmente) se cancelan utilizando el método futuro .cancel() . return_exceptions=True asegura que todas las excepciones asyncio.CancelledError recibidas se almacenen en lugar de causar errores en el futuro.

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