如何由于超时而从已取消的python asyncio协程返回值

问题描述 投票:1回答:1

在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(编辑:这可能是为什么在这里打印回溯,但不在其他地方打印的原因。]

[我已经尝试过在return_exceptions=True中使用asyncio.gather或在CancelledError块中捕获except asyncio.TimeoutError:的许多配置,但我似乎无法正确理解。

我需要将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并在下次等待时返回值?

提前感谢。

python python-3.x exception python-asyncio cancellation
1个回答
0
投票

我进行了一些调试,在asyncio.gather取消的情况下,似乎从未设置结果,因此无法从python 3.8的_GatheringFuture对象中检索到它。

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

几乎在所有情况下都必须重新引发例外。

Imo,python 3.5的行为是无意的。我不会依靠它。

虽然可以通过不使用asyncio.gather来解决此问题,但这样做不值得。如果您确实需要从已取消的协程中获取部分结果,则只需将其添加到某些全局列表中即可:

    except asyncio.CancelledError:
        print("foo got cancelled")
        global_results.append(1)
        raise
© www.soinside.com 2019 - 2024. All rights reserved.