【问题标题】:How to return a value from a cancelled python asyncio coroutine due to timeout由于超时,如何从取消的 python asyncio 协程中返回一个值
【发布时间】:2020-04-04 23:42:30
【问题描述】:

在 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 并在下一次等待时返回一个值?

提前致谢。

【问题讨论】:

    标签: python python-3.x exception python-asyncio cancellation


    【解决方案1】:

    我做了一些调试,看起来在取消 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
    

    【讨论】:

      【解决方案2】:

      感谢@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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-09-07
        • 1970-01-01
        • 1970-01-01
        • 2015-05-31
        • 1970-01-01
        • 2019-12-10
        • 2020-01-07
        相关资源
        最近更新 更多