简体   繁体   English

如何在 Python 的 `asyncio.gather` 中正确处理取消的任务

[英]How to correctly handle cancelled tasks in Python's `asyncio.gather`

So I'm taking another stab at the asyncio module now that 3.8 is out.因此,既然 3.8 已发布,我将再次尝试使用asyncio模块。 However, I am getting unexpected results when trying to do a graceful shutdown of the event loop.但是,在尝试正常关闭事件循环时,我得到了意想不到的结果。 Specifically I am listening for a SIGINT , cancelling the running Task s, gathering those Task s, and then .stop() ing the event loop.具体来说,我正在侦听SIGINT ,取消正在运行的Task ,收集这些Task ,然后.stop()事件循环。 I know that Task s raise a CancelledError when they are cancelled which will propagate up and end my call to asyncio.gather unless, according to the documentation , I pass return_exceptions=True to asyncio.gather , which should cause gather to wait for all the Task s to cancel and return an array of CancelledError s.我知道Task被取消时会引发CancelledError ,这将向上传播并结束我对asyncio.gather调用,除非根据文档,我将return_exceptions=True传递给asyncio.gather ,这应该导致gather等待所有Task s 取消并返回一个CancelledError数组。 However, it appears that return_exceptions=True still results in an immediate interruption of my gather call if I try to gather cancelled Task s.但是,如果我尝试gather已取消的Taskreturn_exceptions=True似乎仍然会导致我的gather调用立即中断。

Here is the code to reproduce the effect.这是重现效果的代码。 I am running python 3.8.0:我正在运行 python 3.8.0:

# demo.py

import asyncio
import random
import signal


async def worker():
    sleep_time = random.random() * 3
    await asyncio.sleep(sleep_time)
    print(f"Slept for {sleep_time} seconds")

async def dispatcher(queue):
    while True:
        await queue.get()
        asyncio.create_task(worker())
        tasks = asyncio.all_tasks()
        print(f"Running Tasks: {len(tasks)}")

async def shutdown(loop):
    tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
    for task in tasks:
        task.cancel()
    print(f"Cancelling {len(tasks)} outstanding tasks")
    results = await asyncio.gather(*tasks, return_exceptions=True)
    print(f"results: {results}")
    loop.stop()

async def main():
    loop = asyncio.get_event_loop()
    loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(loop)))
    queue = asyncio.Queue()
    asyncio.create_task(dispatcher(queue))

    while True:
        await queue.put('tick')
        await asyncio.sleep(1)


asyncio.run(main())

Output:输出:

>> python demo.py 
Running Tasks: 3
Slept for 0.3071352174511871 seconds
Running Tasks: 3
Running Tasks: 4
Slept for 0.4152310498820644 seconds
Running Tasks: 4
^CCancelling 4 outstanding tasks
Traceback (most recent call last):
  File "demo.py", line 38, in <module>
    asyncio.run(main())
  File "/Users/max.taggart/.pyenv/versions/3.8.0/lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/Users/max.taggart/.pyenv/versions/3.8.0/lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
    return future.result()
asyncio.exceptions.CancelledError

I'm guessing there is still something about the event loop that I don't understand, but I would expect all the CancelledError s to come back as an array of objects stored in results and then be able to continue on rather than to see an error immediately.我猜测事件循环仍有一些我不明白的地方,但我希望所有CancelledError都作为存储在results的对象数组返回,然后能够继续而不是看到一个立即出错。

What causes the error?导致错误的原因是什么?

Problem with using asyncio.all_tasks() is that it returns ALL tasks even those you didn't create directly.使用asyncio.all_tasks()是它返回所有任务,即使是那些不是你直接创建的任务。 Change your code following way to see what you cancel:按照以下方式更改您的代码以查看您取消的内容:

for task in tasks:
    print(task)
    task.cancel()

You'll see not only worker related tasks, but also:您不仅会看到与工作worker相关的任务,还会看到:

<Task pending coro=<main() running at ...>

Cancelling main leads to mess inside asyncio.run(main()) and you get error.取消main会导致asyncio.run(main())内部混乱,并且会出错。 Let's do fast/dirty modification to exclude this task from cancellation:让我们进行快速/脏修改以从取消中排除此任务:

tasks = [
    t 
    for t 
    in asyncio.all_tasks() 
    if (
        t is not asyncio.current_task()
        and t._coro.__name__ != 'main'
    )
]

for task in tasks:
    print(task)
    task.cancel()

Now you'll see your results .现在您将看到您的results

loop.stop() leads to error loop.stop() 导致错误

While you achieved results , you will get another error Event loop stopped before Future completed .当您取得results ,您将收到另一个错误Event loop stopped before Future completed It happens because asyncio.run(main()) want to run until main() finished.发生这种情况是因为asyncio.run(main())想要运行直到main()完成。

You have to restructure your code to allow coroutine you passed into asyncio.run be done instead of stopping event loop or, for example, use loop.run_forever() instead of asyncio.run .你必须重构你的代码以允许你传递给asyncio.run协程完成,而不是停止事件循环,或者,例如,使用loop.run_forever()而不是asyncio.run

Here's fast/dirty demonstration of what I mean:这是我的意思的快速/肮脏演示:

async def shutdown(loop):
    # ...

    global _stopping
    _stopping = True
    # loop.stop()

_stopping = False

async def main():
    # ...

    while not _stopping:
        await queue.put('tick')
        await asyncio.sleep(1)

Now your code will work without errors.现在您的代码将正常工作而不会出错。 Don't use code above on practice, it's just an example.不要在实践中使用上面的代码,这只是一个例子。 Try to restructure your code as I mentioned above.尝试按照我上面提到的方式重构您的代码。

How to correctly handle tasks如何正确处理任务

Don't use asyncio.all_tasks() .不要使用asyncio.all_tasks()

If you create some task you want to cancel in the future, store it and cancel only stored tasks.如果您创建了一些要在将来取消的任务,请存储它并仅取消存储的任务。 Pseudo code:伪代码:

i_created = []

# ...

task = asyncio.create_task(worker())
i_created.append(task)

# ...

for task in i_created:
    task.cancel()

It may not seem convenient, but it's a way to make sure you don't cancel something you don't want to be cancelled.这可能看起来不方便,但它是一种确保您不会取消不想被取消的东西的方法。

One more thing还有一件事

Note also that that asyncio.run() does much more than just starting event loop.还需要注意的是asyncio.run()比刚开始事件循环。 In particular, it cancels all hanging tasks before finishing.特别是, 它会在完成之前取消所有挂起的任务。 It may be useful in some cases, although I advise to handle all cancellations manually instead.在某些情况下它可能很有用,但我建议改为手动处理所有取消。

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

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