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?
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
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