Max*_*xpm 13 python resource-cleanup resource-leak cancellation python-asyncio
该contextlib.asynccontextmanager
文档给出了这样的例子:
@asynccontextmanager
async def get_connection():
conn = await acquire_db_connection()
try:
yield conn
finally:
await release_db_connection(conn)
Run Code Online (Sandbox Code Playgroud)
在我看来,这可能会泄漏资源。如果此代码的任务在此代码在其行上时被取消await release_db_connection(conn)
,则发布可能会中断。在asyncio.CancelledError
将从某处内向上传播finally
块,从而防止随后的清理代码运行。
因此,实际上,如果您正在实现一个处理超时请求的 Web 服务器,则在完全错误的时间触发超时可能会导致数据库连接泄漏。
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())
Run Code Online (Sandbox Code Playgroud)
Python 3.7.9 上的输出:
Acquired database connection.
Did stuff with connection.
Done.
Run Code Online (Sandbox Code Playgroud)
请注意,Released database connection
永远不会打印。
.cancel()
的意思是“优雅地取消,清理沿途使用的任何资源”。(否则,他们为什么要将取消实现为异常传播?)但我可能是错的。例如,也许.cancel()
是为了快速而不是优雅。是否有权威来源澄清.cancel()
在这里应该做什么?专注于保护清理工作不被取消是转移注意力的做法。有很多事情可能会出错,而上下文管理器无法知道
\n资源处理实用程序有责任正确处理错误。
\nrelease_db_connection
不能取消,它必须保护自己不被取消。async with
上下文管理器。内部也可能涉及进一步的保护,例如防止取消。async def release_db_connection(conn):\n """\n Cancellation safe variant of `release_db_connection`\n\n Internally protects against cancellation by delaying it until cleanup.\n """\n # cleanup is run in separate task so that it\n # cannot be cancelled from the outside.\n shielded_release = asyncio.create_task(asyncio.sleep(1))\n # Wait for cleanup completion \xe2\x80\x93 unlike `asyncio.shield`,\n # delay any cancellation until we are done.\n try:\n await shielded_release\n except asyncio.CancelledError:\n await shielded_release\n # propagate cancellation when we are done\n raise\n finally:\n print("Released database connection.")\n
Run Code Online (Sandbox Code Playgroud)\n注意:异步清理很棘手。例如,如果事件循环不等待屏蔽任务,那么简单的方法是不够的。asyncio.shield
避免发明自己的保护措施,并依靠底层框架来做正确的事情。
任务的取消是一种正常关闭,a) 仍然允许异步操作,b) 可能会被延迟/抑制。CancelledError
明确允许协程准备处理清理。
\n\n任务.取消
\n然后,协程有机会通过使用块抑制异常来清理甚至拒绝请求
\ntry \xe2\x80\xa6 \xe2\x80\xa6 except CancelledError \xe2\x80\xa6 finally
。[\xe2\x80\xa6]Task.cancel()
不保证任务会被取消,尽管完全抑制取消并不常见,并且是积极劝阻的。
强制关闭是coroutine.close
/ GeneratorExit
。这对应于立即、同步关闭并禁止通过await
、async for
或暂停async with
。
\n\n协程.close
\n[\xe2\x80\xa6] 它
\nGeneratorExit
在暂停点处引发,导致协程立即清理自身。