簡體   English   中英

由於超時,如何從取消的python asyncio協程返回值

[英]How to return a value from a cancelled python asyncio coroutine due to timeout

在python> 3.5中,如何讓協程在因TimeoutError被取消后返回最終值?

我有一個小型 python 項目,它使用多個協程來傳輸數據並報告傳輸的數據量。 它需要一個超時參數; 如果腳本在完成轉移之前超時,它會報告取消前轉移的金額。

它在 python3.5 中運行良好,但最近我嘗試更新到 3.8 並遇到了麻煩。

下面是示例代碼,顯然它的行為與 3.5、3.6、3.7 和 3.8 有很大不同:

import asyncio
import sys


async def foo():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        print("foo got cancelled")
        return 1


async def main():
    coros = asyncio.gather(*(foo() for _ in range(3)))
    try:
        await asyncio.wait_for(coros, timeout=0.1)
    except asyncio.TimeoutError:
        print("main coroutine timed out")
        await coros
    return coros.result()


if __name__ == "__main__":
    print(sys.version)

    loop = asyncio.new_event_loop()
    try:
        results = loop.run_until_complete(main())
        print("results: {}".format(results))
    except Exception as e:
        print("exception in __main__:")
        print(e)
    finally:
        loop.close()
$ for ver in 3.5 3.6 3.7 3.8; do echo; python${ver} example.py; done

3.5.7 (default, Sep  6 2019, 07:49:56)
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)]
main coroutine timed out
foo got cancelled
foo got cancelled
foo got cancelled
results: [1, 1, 1]

3.6.9 (default, Sep  6 2019, 07:45:14)
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)]
main coroutine timed out
foo got cancelled
foo got cancelled
foo got cancelled
exception in __main__:


3.7.4 (default, Sep 17 2019, 13:46:30)
[Clang 10.0.1 (clang-1001.0.46.4)]
foo got cancelled
foo got cancelled
foo got cancelled
main coroutine timed out
exception in __main__:


3.8.0 (default, Oct 16 2019, 21:30:17)
[Clang 11.0.0 (clang-1100.0.33.8)]
foo got cancelled
foo got cancelled
foo got cancelled
main coroutine timed out
Traceback (most recent call last):
  File "example.py", line 28, in <module>
    results = loop.run_until_complete(main())
  File "/usr/local/var/pyenv/versions/3.8.0/lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
    return future.result()
asyncio.exceptions.CancelledError

exception in __main__:不為 3.8 打印,因為CancelledError現在是BaseException而不是Exception (編輯:這可能是回溯打印在這里而不是其他地方的原因)。

我已經嘗試了許多在asyncio.gather中使用return_exceptions=True或在except asyncio.TimeoutError:塊中捕獲CancelledError的配置,但我似乎無法做到正確。

我需要將main作為異步函數保留,因為在我的實際代碼中,它正在為其他協程共享創建 aiohttp 會話,而現代 aiohttp 要求在異步上下文管理器(而不是常規同步上下文管理器)中完成此操作。

我希望在 3.5-3.8 上運行的代碼,所以我沒有使用asyncio.run

我已經嘗試了許多其他問題的代碼,這些問題使用.cancel()有或沒有contextlib.suppress(asyncio.CancelledError) ,但仍然沒有運氣。 我也試過返回一個等待的值(例如result = await coros; return result而不是return coros.result() ),也沒有骰子。

有沒有一種好方法可以讓我在 python > 3.5 中獲得 python 3.5 的行為,其中我可以讓協程在超時時捕獲CancelledError並在下一次等待時返回一個值?

提前致謝。

我做了一些調試,看起來結果在 asyncio.gather 取消的情況下從未設置過,因此無法從 python 3.8 中的_GatheringFuture對象中檢索它。

異步/tasks.py:792

            if outer._cancel_requested:
                # If gather is being cancelled we must propagate the
                # cancellation regardless of *return_exceptions* argument.
                # See issue 32684.
                outer.set_exception(exceptions.CancelledError())
            else:
                outer.set_result(results)

asyncio.CancelledError文檔我發現這個關於asyncio.CancelledError

在幾乎所有情況下,都必須重新引發異常。

Imo,python 3.5 的行為是無意的。 我不會依賴它。

雖然可以通過不使用asyncio.gather來解決這個asyncio.gather ,但這樣做並不值得。 如果您確實需要從取消的協程中獲取部分結果,那么只需將其添加到某個全局列表中:

    except asyncio.CancelledError:
        print("foo got cancelled")
        global_results.append(1)
        raise

感謝@RafalS 和他們建議停止使用asyncio.gather

而不是使用gatherwait_for ,似乎將.wait的超時直接與協程一起使用可能是最好的選擇,並且適用於 3.5 到 3.8。

請注意,下面的 bash 命令略有修改,以顯示任務正在同時運行,並且在不等待foo完成的情況下也被取消。

import asyncio
import sys


async def foo():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        pass
    finally:
        return 1


async def main():
    coros = [foo() for _ in range(3)]
    done, pending = await asyncio.wait(coros, timeout=1.0)
    for task in pending:
        task.cancel()
        await task
    return [task.result() for task in done | pending]


if __name__ == "__main__":
    print(sys.version)

    loop = asyncio.new_event_loop()
    try:
        results = loop.run_until_complete(main())
        print("results: {}".format(results))
    finally:
        loop.close()
$ for ver in 3.5 3.6 3.7 3.8; do echo; time python${ver} example.py; done

3.5.7 (default, Sep  6 2019, 07:49:56)
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)]
results: [1, 1, 1]

real    0m1.634s
user    0m0.173s
sys     0m0.106s

3.6.9 (default, Sep  6 2019, 07:45:14)
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)]
results: [1, 1, 1]

real    0m1.643s
user    0m0.184s
sys     0m0.100s

3.7.4 (default, Sep 17 2019, 13:46:30)
[Clang 10.0.1 (clang-1001.0.46.4)]
results: [1, 1, 1]

real    0m1.499s
user    0m0.129s
sys     0m0.089s

3.8.0 (default, Oct 16 2019, 21:30:17)
[Clang 11.0.0 (clang-1100.0.33.8)]
results: [1, 1, 1]

real    0m1.492s
user    0m0.141s
sys     0m0.087s

暫無
暫無

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

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