La documentación contextlib.asynccontextmanager da este ejemplo:
@asynccontextmanager async def get_connection(): conn = await acquire_db_connection() try: yield conn finally: await release_db_connection(conn) Me parece que esto puede filtrar recursos. Si la tarea de este código se cancela mientras este código está en su línea await release_db_connection(conn) , la liberación podría interrumpirse. El asyncio.CancelledError se propagará desde algún lugar dentro del bloque finally , evitando que se ejecute el código de limpieza posterior.
Entonces, en términos prácticos, si está implementando un servidor web que maneja solicitudes con un tiempo de espera, un tiempo de espera que se activa en el momento exacto incorrecto podría causar una fuga en la conexión de la base de datos.
import asyncio from contextlib import asynccontextmanager async def acquire_db_connection(): await asyncio.sleep(1) print("Acquired database connection.") return "<fake connection object>" async def release_db_connection(conn): await asyncio.sleep(1) print("Released database connection.") @asynccontextmanager async def get_connection(): conn = await acquire_db_connection() try: yield conn finally: await release_db_connection(conn) async def do_stuff_with_connection(): async with get_connection() as conn: await asyncio.sleep(1) print("Did stuff with connection.") async def main(): task = asyncio.create_task(do_stuff_with_connection()) # Cancel the task just as the context manager running # inside of it is executing its cleanup code. await asyncio.sleep(2.5) task.cancel() try: await task except asyncio.CancelledError: pass print("Done.") asyncio.run(main())Salida en Python 3.7.9:
Acquired database connection. Did stuff with connection. Done. Tenga en cuenta que la Released database connection nunca se imprime.
.cancel() signifique "cancelar con gracia, limpiando cualquier recurso utilizado en el camino". (De lo contrario, ¿por qué habrían implementado la cancelación como propagación de excepciones?) Pero podría estar equivocado. Tal vez, por ejemplo, .cancel() esté destinado a ser rápido en lugar de elegante. ¿Hay alguna fuente autorizada que aclare qué se supone que debe hacer .cancel() aquí?Puede proteger la tarea con asyncio.shield para garantizar el apagado correcto del administrador de contexto, hice cambios solo en main() :
async def main(): task = asyncio.create_task(do_stuff_with_connection()) # shield context manager from cancellation sh_task = asyncio.shield(task) # Cancel the task just as the context manager running # inside of it is executing its cleanup code. await asyncio.sleep(2.5) sh_task.cancel() # cancel shielded task try: await sh_task except asyncio.CancelledError: pass await asyncio.sleep(5) # wait till shielded task is done print("Done.")Centrarse en proteger la limpieza de la cancelación es una pista falsa. Hay una multitud de cosas que pueden salir mal y el administrador de contexto no tiene forma de saber
Es responsabilidad de las utilidades de manejo de recursos manejar adecuadamente los errores.
release_db_connection , debe protegerse contra la cancelación.async with el administrador de contexto. La protección adicional, por ejemplo, contra la cancelación, también puede estar involucrada internamente. async def release_db_connection(conn): """ Cancellation safe variant of `release_db_connection` Internally protects against cancellation by delaying it until cleanup. """ # cleanup is run in separate task so that it # cannot be cancelled from the outside. shielded_release = asyncio.create_task(asyncio.sleep(1)) # Wait for cleanup completion – unlike `asyncio.shield`, # delay any cancellation until we are done. try: await shielded_release except asyncio.CancelledError: await shielded_release # propagate cancellation when we are done raise finally: print("Released database connection.") Nota : la limpieza asincrónica es complicada. Por ejemplo, un asyncio.shield simple no es suficiente si el bucle de eventos no espera tareas protegidas. Evite inventar su propia protección y confíe en los marcos subyacentes para hacer lo correcto.
La cancelación de una tarea es un apagado correcto que a) todavía permite operaciones asíncronas y b) puede retrasarse/suprimirse. Las corrutinas que se preparan para manejar CancelledError para la limpieza están explícitamente permitidas.
Tarea.cancelar
Luego, la corrutina tiene la oportunidad de limpiar o incluso denegar la solicitud al suprimir la excepción con un
try … … except CancelledError … finallybloquear. […]Task.cancel()no garantiza que la tarea se cancelará, aunque suprimir la cancelación por completo no es común y se desaconseja activamente.
Un apagado forzado es coroutine.close / GeneratorExit . Esto corresponde a un apagado síncrono inmediato y prohíbe la suspensión a través de await , async for o async with .
coroutine.cerrar
[…] eleva
GeneratorExiten el punto de suspensión, lo que hace que la corrutina se limpie de inmediato.