这是我的代码的简化版本:main
是在第二次迭代后停止的协程。get_numbers
是一个异步生成器,它生成数字,但在异步上下文管理器中。
import asyncio
class MyContextManager:
async def __aenter__(self):
print("Enter to the Context Manager...")
return self
async def __aexit__(self, exc_type, exc_value, exc_tb):
print(exc_type)
print("Exit from the Context Manager...")
await asyncio.sleep(1)
print("This line is not executed") # <-------------------
await asyncio.sleep(1)
async def get_numbers():
async with MyContextManager():
for i in range(30):
yield i
async def main():
async for i in get_numbers():
print(i)
if i == 1:
break
asyncio.run(main())
输出为:
Enter to the Context Manager...
0
1
<class 'asyncio.exceptions.CancelledError'>
Exit from the Context Manager...
我其实有两个问题:
1.根据我的理解,AsyncIO在事件循环的下一个循环中安排了一个Task,并给__aexit__
一个执行的机会。但是print("This line is not executed")
这一行没有执行。为什么呢?假设如果我们在__aexit__
中有一个await
语句,这行之后的代码根本不会执行,我们不应该依靠它来清理?
1.异步生成器上的help()
的输出显示:
| aclose(...)
| aclose() -> raise GeneratorExit inside generator.
那么为什么我在__aexit__
中得到<class 'asyncio.exceptions.CancelledError'>
异常呢?
- 使用Python 3.10.4
4条答案
按热度按时间fhg3lkii1#
这并不是针对
__aexit__
,而是针对所有异步代码:当一个事件循环关闭时,它必须在 * 取消 * 剩余任务或 * 保留 * 它们之间做出决定。为了清理,大多数框架更喜欢取消而不是依赖于程序员稍后清理保留的任务。这种关闭清理是一种独立的机制,与正常执行期间调用堆栈上函数、上下文等的优雅展开不同。* 在取消期间也必须清理的上下文管理器必须专门为此做好准备 *。尽管如此,在许多情况下,不为此做准备也是可以的,因为许多资源本身是故障安全的。
在当代的事件循环框架中,通常有三个级别的清理:
__aexit__
被调用,并且可能会收到一个触发展开的异常作为参数。清理预计会根据需要延迟很长时间。这与运行同步代码的__exit__
相当。__aexit__
可能会接收CancelledError
1作为参数 * 或任何await
/async for
/async with
* 上的异常。清理可能会延迟此操作,但预计会尽可能快地进行。这相当于KeyboardInterrupt
取消同步代码。__aexit__
可能会接收GeneratorExit
作为参数 * 或作为任何await
/async for
/async with
* 上的异常。清理必须尽快进行。这与GeneratorExit
关闭同步发电机类似。要处理取消/关闭,任何
async
代码(无论是在__aexit__
中还是在其他地方)都必须处理CancelledError
或GeneratorExit
。虽然前者可能会被延迟或抑制,但后者应该立即同步处理2。具体来说,异步生成器的清理是一个棘手的问题,因为它们可以一次被所有情况清理:当生成器完成时展开,当拥有任务被销毁时取消,或者当生成器被垃圾收集时关闭。* 清除信号到达的顺序取决于实现。*
解决这个问题的正确方法不是首先依赖于隐式清理。相反,每个协程都应该确保在父进程退出之前关闭其所有子进程资源。值得注意的是,异步生成器可能持有资源并需要关闭。
在最近的版本中,此模式通过
aclosing
上下文管理器进行编码。1此例外的名称和/或标识可能会有所不同。
2虽然在
GeneratorExit
期间可以await
异步操作,但它们可能不会屈服于事件循环。同步接口有利于强制执行这一点。balp4ylt2#
我不知道发生了什么,但张贴我发现的情况下,它证明是有用的其他人决定调查.当我们存储引用
get_numbers()
外部main()
的输出变化为预期.我会说,它似乎get_numbers()
是垃圾收集到早期,但禁用gc
没有帮助,所以我的猜测可能是错误的.zpgglvta3#
答案很简单解释器将在一秒后继续执行
__aexit__
,但是main
函数完成并且没有指向上下文管理器的指针。你提到的第一个明显的解决方案是在main函数之后等待足够长的时间:
另一种方法是使用try/finally:
r7s23pms4#
回答第一个问题:
假设如果我们在
__aexit__
中有一个await语句,那么该行之后的代码根本不会执行,这是正确的吗?我会说不,并不总是这样。只要
main
有足够的时间,并且可以再次将控制权传递回事件循环,__aexit__
内部的代码就可以执行。我试过这样做:.run()
只关心传递给它的协程并将其运行到最后,而不关心包括__aexit__
在内的其他协程。因此,如果它没有足够的时间或没有将控制权传递给事件循环,我不能依赖第一个await语句之后的下一行。可能有帮助的其他信息:
在base_events.py/
run_forever
方法(由.run()
调用)中,我发现self._asyncgen_finalizer_hook
被传递给sys.set_asyncgen_hooks
。_asyncgen_finalizer_hook
的主体是:但是
call_soon_threadsafe
的实现是空的。稍后我将整理这个答案并删除这些猜测。