簡體   English   中英

如何在 Python 的 `asyncio.gather` 中正確處理取消的任務

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

因此,既然 3.8 已發布,我將再次嘗試使用asyncio模塊。 但是,在嘗試正常關閉事件循環時,我得到了意想不到的結果。 具體來說,我正在偵聽SIGINT ,取消正在運行的Task ,收集這些Task ,然后.stop()事件循環。 我知道Task被取消時會引發CancelledError ,這將向上傳播並結束我對asyncio.gather調用,除非根據文檔,我將return_exceptions=True傳遞給asyncio.gather ,這應該導致gather等待所有Task s 取消並返回一個CancelledError數組。 但是,如果我嘗試gather已取消的Taskreturn_exceptions=True似乎仍然會導致我的gather調用立即中斷。

這是重現效果的代碼。 我正在運行 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())

輸出:

>> 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

我猜測事件循環仍有一些我不明白的地方,但我希望所有CancelledError都作為存儲在results的對象數組返回,然后能夠繼續而不是看到一個立即出錯。

導致錯誤的原因是什么?

使用asyncio.all_tasks()是它返回所有任務,即使是那些不是你直接創建的任務。 按照以下方式更改您的代碼以查看您取消的內容:

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

您不僅會看到與工作worker相關的任務,還會看到:

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

取消main會導致asyncio.run(main())內部混亂,並且會出錯。 讓我們進行快速/臟修改以從取消中排除此任務:

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()

現在您將看到您的results

loop.stop() 導致錯誤

當您取得results ,您將收到另一個錯誤Event loop stopped before Future completed 發生這種情況是因為asyncio.run(main())想要運行直到main()完成。

你必須重構你的代碼以允許你傳遞給asyncio.run協程完成,而不是停止事件循環,或者,例如,使用loop.run_forever()而不是asyncio.run

這是我的意思的快速/骯臟演示:

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)

現在您的代碼將正常工作而不會出錯。 不要在實踐中使用上面的代碼,這只是一個例子。 嘗試按照我上面提到的方式重構您的代碼。

如何正確處理任務

不要使用asyncio.all_tasks()

如果您創建了一些要在將來取消的任務,請存儲它並僅取消存儲的任務。 偽代碼:

i_created = []

# ...

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

# ...

for task in i_created:
    task.cancel()

這可能看起來不方便,但它是一種確保您不會取消不想被取消的東西的方法。

還有一件事

還需要注意的是asyncio.run()比剛開始事件循環。 特別是, 它會在完成之前取消所有掛起的任務。 在某些情況下它可能很有用,但我建議改為手動處理所有取消。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM