为什么 __aexit__ 里面有await 却没有完全执行?

Sor*_*ary 12 python asynchronous contextmanager python-3.x python-asyncio

这是我的代码的简化版本:

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())
Run Code Online (Sandbox Code Playgroud)

输出是:

Enter to the Context Manager...
0
1
<class 'asyncio.exceptions.CancelledError'>
Exit from the Context Manager...
Run Code Online (Sandbox Code Playgroud)

我其实有两个问题:

  1. 根据我的理解,AsyncIO 安排一个任务在事件循环的下一个周期中很快被调用,并给出__aexit__执行的机会。但该行print("This line is not executed")没有被执行。这是为什么?假设如果我们await在 中有一条语句__aexit__,那么该行之后的代码根本不会执行,并且我们不应该依赖它来进行清理,这样的假设是否正确?

  1. 异步生成器的输出help()表明:
 |  aclose(...)
 |      aclose() -> raise GeneratorExit inside generator.
Run Code Online (Sandbox Code Playgroud)

那么为什么我<class 'asyncio.exceptions.CancelledError'>在 里面遇到异常__aexit__

* 我使用的是Python 3.10.4

Mis*_*agi 5

这不是特定于__aexit__而是所有异步代码:当事件循环关闭时,它必须决定是取消剩余任务还是保留它们。为了清理的目的,大多数框架更喜欢取消,而不是依赖程序员稍后清理保留的任务。

\n

这种关闭清理是一种独立的机制,与正常执行期间调用堆栈上的函数、上下文和类似内容的正常展开不同。还必须在取消期间进行清理的上下文管理器必须为此做好专门准备。不过,在许多情况下,不为此做好准备也没有什么问题,因为许多资源会自行失效。

\n
\n

在当代的事件循环框架中,通常存在三个级别的清理:

\n
    \n
  • 展开:__aexit__当范围结束时调用,并且可能会收到触发展开作为参数的异常。预计清理工作将根据需要推迟。这与运行同步代码相当__exit__
  • \n
  • 取消:__aexit__可以接收CancelledError1作为参数或作为任何//异常awaitasync forasync with。清理工作可能会延迟,但预计会尽快进行。这与取消同步代码相当KeyboardInterrupt
  • \n
  • 结束语:__aexit__可以接收 aGeneratorExit作为参数或作为任何//异常awaitasync forasync with。清理工作必须尽快进行。这相当于GeneratorExit关闭同步发电机。
  • \n
\n

要处理取消/关闭,任何async代码 \xe2\x80\x93 无论是在__aexit__\xe2\x80\x93 中还是其他地方,都必须期望处理CancelledErroror GeneratorExit。前者可能会被延迟或抑制,但后者应该立即、同步处理2

\n
    async def __aexit__(self, exc_type, exc_value, exc_tb):\n        print("Exit from the Context Manager...")\n        try:\n            await asyncio.sleep(1)  # an exception may arrive here\n        except GeneratorExit:\n            print("Exit stage left NOW")\n            raise\n        except asyncio.CancelledError:\n            print("Got cancelled, just cleaning up a few things...")\n            await asyncio.sleep(0.5)\n            raise\n        else:\n            print("Nothing to see here, taking my time on the way out")\n            await asyncio.sleep(1)\n
Run Code Online (Sandbox Code Playgroud)\n

注意:通常不可能详尽地处理这些情况。不同形式的清理可能会相互中断,例如取消展开然后关闭。只有尽最大努力才能进行清理;强大的清理是通过故障安全来实现的,例如通过事务,而不是显式清理。

\n
\n

具体来说,异步生成器的清理是一个棘手的情况,因为它们可以在所有情况下同时清理:在生成器完成时展开,在拥有任务被销毁时取消,或者在生成器被垃圾收集时关闭。清理信号到达的顺序取决于实现。

\n

解决这个问题的正确方法是首先不要依赖隐式清理。相反,每个协程应确保其所有子资源在父级退出之前关闭。值得注意的是,异步生成器可能会占用资源并需要关闭。

\n
    async def __aexit__(self, exc_type, exc_value, exc_tb):\n        print("Exit from the Context Manager...")\n        try:\n            await asyncio.sleep(1)  # an exception may arrive here\n        except GeneratorExit:\n            print("Exit stage left NOW")\n            raise\n        except asyncio.CancelledError:\n            print("Got cancelled, just cleaning up a few things...")\n            await asyncio.sleep(0.5)\n            raise\n        else:\n            print("Nothing to see here, taking my time on the way out")\n            await asyncio.sleep(1)\n
Run Code Online (Sandbox Code Playgroud)\n

aclosing在最新版本中,此模式通过上下文管理器进行编码。

\n
async def main():\n    # create a generator that might need cleanup\n    async_iter = get_numbers()\n    async for i in async_iter:\n        print(i)\n        if i == 1:\n            break\n    # wait for generator clean up before exiting\n    await async_iter.aclose()\n
Run Code Online (Sandbox Code Playgroud)\n
\n

1此例外的名称和/或身份可能有所不同。

\n

2虽然可以await在 期间进行异步处理GeneratorExit,但它们可能不会屈服于事件循环。同步接口有利于强制执行此操作。

\n