简体   繁体   English

Python asyncio 任务中未处理的异常后整理

[英]Tidying up after unhandled exception in Python asyncio task

In a python aysncio application comprising multiple tasks, what is the correct way to report an unhandled exception in one of the tasks then exit the application?在包含多个任务的 python aysncio 应用程序中,在其中一个任务中报告未处理异常然后退出应用程序的正确方法是什么? I have looked at several related questions, this one in particular:我看过几个相关的问题,特别是这个:

How to shutdown the loop and print error if coroutine raised an exception with asyncio? 如果协程使用 asyncio 引发异常,如何关闭循环并打印错误?

... but the best that gets me is shutting the application down with runtime errors. ...但让我感到最好的是关闭应用程序并出现运行时错误。

For example, the following code will create 4 tasks then run a main loop.例如,以下代码将创建 4 个任务,然后运行一个主循环。 The tasks will throw an exception (AssertionError) after a period of time, to simulate a "this should never happen" event which I need to trigger an orderly shutdown of the application.这些任务将在一段时间后抛出异常(AssertionError),以模拟“这永远不会发生”事件,我需要触发应用程序的有序关闭。

At present, this code will trigger the exception within its task, and that task will abort.目前,此代码将在其任务中触发异常,并且该任务将中止。 Without the custom exception handler the other tasks will continue, but with it at least the application will abort, but it will do so with "RuntimeError: Event loop stopped before Future completed."如果没有自定义异常处理程序,其他任务将继续,但至少应用程序将中止,但它会使用“RuntimeError:事件循环在 Future 完成之前停止”。

I'm testing in Python 3.7我正在测试 Python 3.7

#!/usr/bin/python3
import asyncio

class Test:
    def __init__(self, loop):
        loop.create_task( self.test(3))
        loop.create_task( self.test(4))
        loop.create_task( self.test(5))
        loop.create_task( self.test(7))

    def __enter__(self):
        return self
    def __exit__(self, type, value, tb):
        pass

    async def test(self, max):
        i = 0
        while True:
            i = i + 1
            print("Loop %d of %d" %(i,max))
            assert i < max
            await asyncio.sleep(1)


    async def main(self):
        while True:
            print("Main loop doing stuff")
            await asyncio.sleep(0.5)


def custom_exception_handler(loop, context):
    # first, handle with default handler
    loop.default_exception_handler(context)
    loop.stop()

loop = asyncio.get_event_loop()
loop.set_exception_handler(custom_exception_handler)

with Test(loop) as t:
    loop.run_until_complete( t.main() )

Instead of stopping the loop in your event handler, just catch the exception in test() and terminate main() gracefully.无需在事件处理程序中停止循环,只需在 test() 中捕获异常并优雅地终止 main()。 You see a traceback for the assertion error because the default exception handler does that.您会看到断言错误的回溯,因为默认异常处理程序会执行此操作。 Everything else will happen automatically thanks to the run_until_complete function, and you get no RuntimeExceptions or warnings.由于 run_until_complete function,其他所有事情都会自动发生,并且您不会收到 RuntimeExceptions 或警告。

In general you don't want an exception in Task A to shutdown other tasks, but your application logic may require that sort of thing.一般来说,您不希望任务 A 中的异常关闭其他任务,但您的应用程序逻辑可能需要这种事情。 In that case you need to explicitly code a mechanism to do that.在这种情况下,您需要显式地编写一种机制来执行此操作。 Usually there is a better way to terminate the loop than loop.stop(), unless you started the loop with run_forever().通常有比 loop.stop() 更好的方法来终止循环,除非你用 run_forever() 开始循环。

You might also look into Task.cancel(), which could be useful in more complex cases.您还可以查看 Task.cancel(),这在更复杂的情况下可能很有用。 If you decide to use that function, the task being cancelled should handle asyncio.CancelledError for a graceful termination.如果您决定使用该 function,则被取消的任务应处理 asyncio.CancelledError 以正常终止。 Note that is NOT a subclass of Exception but of RuntimeException, a little detail that tripped me up the first time.请注意,它不是 Exception 的子类,而是 RuntimeException 的子类,这是第一次让我感到困惑的一个小细节。

I used Python3.8 on Win10, as my code indicates, but I don't think that will matter.正如我的代码所示,我在 Win10 上使用了 Python3.8,但我认为这并不重要。

#! python3.8

import asyncio

class Test:
    def __init__(self, loop):
        self.running = True
        loop.create_task(self.test(3))
        loop.create_task(self.test(4))
        loop.create_task(self.test(5))
        loop.create_task(self.test(7))

    def __enter__(self):
        return self

    def __exit__(self, *_x):
        pass

    async def test(self, mx):
        try:
            i = 0
            while True:
                i = i + 1
                print("Loop %d of %d" %(i, mx))
                assert i < mx
                await asyncio.sleep(1)
        except Exception:
            self.running = False
            raise

    async def main(self):
        while self.running:
            print("Main loop doing stuff")
            await asyncio.sleep(0.5)

loop = asyncio.get_event_loop()

with Test(loop) as t:
    loop.run_until_complete(t.main())

Here is a second solution using the Task.cancel() function:这是使用 Task.cancel() function 的第二种解决方案:

#! python3.8

import asyncio

class Test:
    def __init__(self, loop):
        self.main_task = loop.create_task(self.main())
        loop.set_exception_handler(self.custom_exception_handler)
        loop.create_task(self.test(3))
        loop.create_task(self.test(4))
        loop.create_task(self.test(5))
        loop.create_task(self.test(7))

    def __enter__(self):
        return self

    def __exit__(self, *_x):
        pass

    async def test(self, mx):
        i = 0
        while True:
            i = i + 1
            print("Loop %d of %d" %(i, mx))
            assert i < mx
            await asyncio.sleep(1)

    async def main(self):
        try:
            while True:
                print("Main loop doing stuff")
                await asyncio.sleep(0.5)
        except asyncio.CancelledError:
            pass

    def custom_exception_handler(self, loop, context):
        # first, handle with default handler
        loop.default_exception_handler(context)
        self.main_task.cancel()

loop = asyncio.get_event_loop()

with Test(loop) as t:
    loop.run_until_complete(t.main_task)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM