如何在Python asyncio后台任务中引发异常

4

问题

我有几个持续运行的任务,但其中一个偶尔需要重新启动,因此这个任务在后台运行。如何立即引发来自此后台任务的异常?在以下示例中,异常直到下一次尝试重新启动时才被引发,这在实际应用程序中可能非常不频繁,因此可能会长时间不被注意。

示例

https://replit.com/@PatrickPei/how-to-raise-exceptions-in-python-asyncio-background-task

这个例子运行了3个任务:

  1. foo,不会引发任何异常
  2. bar,在6次迭代后引发异常
  3. on_interval,每5秒重新启动bar
import asyncio

task = None
i = 0


async def foo():
    while True:
        print("foo")
        await asyncio.sleep(1)


async def bar():
    while True:
        global i
        i += 1
        if i > 4:
            raise ValueError()

        print("bar", i)
        await asyncio.sleep(1)


async def on_interval(n):
    while True:
        await asyncio.sleep(n)

        # Cancel task.
        global task
        print("Canceling bar")
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass

        # Restart task.
        print("Restarting bar")
        task = asyncio.create_task(bar())


async def main():
    # Start background task.
    print("Starting bar")
    global task
    task = asyncio.create_task(bar())

    # Start other tasks.
    await asyncio.gather(
        foo(),
        on_interval(3),
    )


if __name__ == "__main__":
    asyncio.run(main())

输出

bar 迭代了 4 次并引发了一个异常,直到下一次重启才被捕获,正如在 bar 4 后显示的 3 次 foo 迭代。当重启之间有很长时间时,这是一个问题,因为异常会长时间不被注意。

Starting bar
bar 1
foo
bar 2
foo
bar 3
foo
Canceling bar
Restarting bar
bar 4
foo
foo
foo
Canceling bar
Traceback (most recent call last):
  File "~/example.py", line 60, in <module>
    asyncio.run(main())
  File "~/.pyenv/versions/3.10.5/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "~/.pyenv/versions/3.10.5/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete
    return future.result()
  File "~/example.py", line 53, in main
    await asyncio.gather(
  File "~/example.py", line 37, in on_interval
    await task
  File "~/example.py", line 22, in bar
    raise ValueError()
ValueError

尝试

  • 开始另一个任务来检查asyncio.Task.exception,但是这很麻烦,因为每个后台任务都需要另一个繁忙的循环来帮助引发它的异常。
  • 尝试asyncio.Task.add_done_callback,但由于后台任务直到下一次重启之前仍未等待,因此它只记录错误而不停止其他任务foo
1个回答

3
在Python 3.11中,使用async with异步上下文管理器和asyncio.TaskGroup()可以简单地解决这个问题。
import asyncio

i = 0


async def foo():
    while True:
        print("foo")
        await asyncio.sleep(1)


async def bar():
    while True:
        global i
        i += 1
        if i > 14:
            raise ValueError()

        print("bar", i)
        await asyncio.sleep(1)


async def on_interval(n):
    while True:
        async with asyncio.TaskGroup() as tg1:
            print("Restarting bar")
            task2 = tg1.create_task(bar())

            await asyncio.sleep(n)

            print("Canceling bar")
            task2.cancel()
         


async def main():
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(foo())
        task3 = tg.create_task(on_interval(3))


asyncio.run(main())

-------------------------

foo
Restarting bar
bar 1
foo
bar 2
foo
bar 3
Canceling bar
foo
Restarting bar
bar 4
  + Exception Group Traceback (most recent call last):
  |   File "C:\Users\ф\PycharmProjects\tkinter\rrr.py", line 42, in <module>
  |     asyncio.run(main())
  |   File "C:\Users\ф\AppData\Local\Programs\Python\Python311\Lib\asyncio\runners.py", line 190, in run
  |     return runner.run(main)
  |            ^^^^^^^^^^^^^^^^
  |   File "C:\Users\ф\AppData\Local\Programs\Python\Python311\Lib\asyncio\runners.py", line 118, in run
  |     return self._loop.run_until_complete(task)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\Users\ф\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_events.py", line 650, in run_until_complete
  |     return future.result()
  |            ^^^^^^^^^^^^^^^
  |   File "C:\Users\ф\PycharmProjects\tkinter\rrr.py", line 37, in main
  |     async with asyncio.TaskGroup() as tg:
  |   File "C:\Users\ф\AppData\Local\Programs\Python\Python311\Lib\asyncio\taskgroups.py", line 135, in __aexit__
  |     raise me from None
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Exception Group Traceback (most recent call last):
    |   File "C:\Users\ф\PycharmProjects\tkinter\rrr.py", line 25, in on_interval
    |     async with asyncio.TaskGroup() as tg1:
    |   File "C:\Users\ф\AppData\Local\Programs\Python\Python311\Lib\asyncio\taskgroups.py", line 135, in __aexit__
    |     raise me from None
    | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
    +-+---------------- 1 ----------------
      | Traceback (most recent call last):
      |   File "C:\Users\ф\PycharmProjects\tkinter\rrr.py", line 17, in bar
      |     raise ValueError()
      | ValueError
      +------------------------------------
foo

Process finished with exit code 1

对此进行了微调,将后台任务和有条件重新启动它的任务分开:https://replit.com/@PatrickPei/6407261 感谢您的帮助! - Patrick Pei

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接