为什么`asyncio.run()` 在 Python 3.8 中永远不会返回?

Jim*_*eld 3 python exception cancellation python-asyncio

我的应用程序具有以下顶级代码(略有缩写):

def main():
    # Some setup code ...
    try:
        asyncio.run(my_coroutine())
    except Exception as e:
        print("Exiting due to exception {}: {}".format(type(e).__name__, e))
    print("Coroutine finished")
    # Some cleanup code ...
    print("Shutdown complete")
Run Code Online (Sandbox Code Playgroud)

在 Python 3.8 中asyncio.run永远不会完成,因此清理代码不会运行(并且不会打印关闭文本)。当应用程序应该退出时,它只是永远挂起。在 Python 3.7 中它运行良好。Python的3.6没有asyncio.run,但相当类似的代码loop.run_until_complete()loop.close()工作过。

一些额外的上下文:设置和清理代码启动并优雅地退出使用threading.Thread. 正是这个线程实际上停止了主协程:它取消了协程(从技术上讲,它取消了在 内调用的另一个协程my_coroutine()loop.call_soon_threadsafe

Jim*_*eld 6

问题的前提是错误的。(我知道是在我写的时候而不是在我最初遇到问题的时候,所以我把它留在这种形式给其他有同样问题的人。)asyncio.run() 正在完成,但跳过所有异常处理和关闭代码:

def main():
    # Some setup code ...
    try:
        asyncio.run(my_coroutine())
    except Exception as e:
        # <---- In Python 3.7, control passed to here when asyncio.run() finshed
        print("Exiting due to exception {}: {}".format(type(e).__name__, e))
    print("Coroutine finished")
    # Some cleanup code ...
    print("Shutdown complete")
    # <---- In Python 3.8 it directly dropped out of here!
Run Code Online (Sandbox Code Playgroud)

这是因为,在 Python 3.8 中, 的基类asyncio.CancelledErrorException变成了BaseException(它是 的基类Exception)。这样做是为了避免异步代码中的错误,因为人们Exception认为这意味着某些操作失败(例如网络错误)但意外阻止了取消;参见Python 问题 32528

然后应用程序无法退出,因为 Python 等待所有线程完成,除非它们开始时daemon=False传递给Thread构造函数(请参阅Thread 对象文档中的几段讨论)。在我的情况下,该线程不是守护线程(因为我确实希望它优雅地完成)但除非我要求它,否则它不会退出,这是在代码中完成的,我在其中添加了注释“一些清理代码...... ”。

解决方案是asyncio.CancelledError在 之外显式捕获Exception,或者使用finally:块代替。该finally:块可能是更好的,因为它确实保证了代码将被运行,即使在源自其他异常的脸BaseException,如KeyboardInterrupt

def main():
    # Some setup code ...
    try:
        asyncio.run(my_coroutine())
    finally:
        print("Coroutine finished")
        # Some cleanup code ...
        print("Shutdown complete")
Run Code Online (Sandbox Code Playgroud)