【问题标题】:Two async operations with one timeout两个异步操作,一个超时
【发布时间】:2018-09-25 12:16:56
【问题描述】:

基本上我想要:

await action1()
await action2()
return result

两个操作都有一个超时,并且 - 这很重要 - 有一条错误消息告诉哪个操作超时。

为了比较,只需一个操作:

try:
    await asyncio.wait_for(action(), timeout=1.0)
except asyncio.TimeoutError:
    raise RuntimeError("Problem")

现在有两个动作我有这个但不喜欢它。

import asyncio

async def a2():
    try:
        await asyncio.sleep(1.0)
    except asyncio.CancelledError:
        raise RuntimeError("Problem 1") from None
    try:
        await asyncio.sleep(1.0)
    except asyncio.CancelledError:
        raise RuntimeError("Problem 2") from None
    return True


async def test():
    loop = asyncio.get_event_loop()
    action_task = loop.create_task(a2())
    # timeouts: 0.5 -> Problem1 exc; 1.5 -> Problem2 exc; 2.5 -> OK
    try:
        await asyncio.wait_for(action_task, timeout=0.5)
    except asyncio.TimeoutError:
        pass
    result = await action_task

asyncio.get_event_loop().run_until_complete(test())

我觉得拥有以下内容真的违反直觉:

except asyncio.TimeoutError:
     pass

超时处理是主要功能。你能推荐一个更好的方法吗?

【问题讨论】:

标签: python python-asyncio


【解决方案1】:

你能推荐一个更好的方法吗?

您的代码是正确的,但如果您正在寻找更优雅的东西,也许上下文管理器会适合您的使用:

class Timeout:
    def __init__(self, tmout):
        self.tmout = tmout
        self._timed_out = False
        self._section = None

    async def __aenter__(self):
        loop = asyncio.get_event_loop()
        self._timer = loop.call_later(self.tmout, self._cancel_task,
                                      asyncio.current_task())
        return self

    def set_section(self, section):
        self._section = section

    def _cancel_task(self, task):
        self._timed_out = True
        task.cancel()

    async def __aexit__(self, t, v, tb):
        if self._timed_out:
            assert t is asyncio.CancelledError
            raise RuntimeError(self._section) from None
        else:
            self._timer.cancel()

人们会这样使用它:

async def main():
    # raises RuntimeError("second sleep")
    async with Timeout(1) as tmout:
        tmout.set_section("first sleep")
        # increase above 1.0 and "first sleep" is raised
        await asyncio.sleep(0.8)
        tmout.set_section("second sleep")
        await asyncio.sleep(0.5)

asyncio.get_event_loop().run_until_complete(main())

【讨论】:

  • 在过去的几周里,我从你那里得到了几个有用的 asyncio 答案。谢谢!
  • @VPfB 感谢您提出有趣的问题!
【解决方案2】:

最初为aiohttp 开发的async_timeout 模块可能正是您需要的。它的回溯包含导致超时的行。

安装:

pip install async_timeout

用法:

import asyncio
from async_timeout import timeout


async def main():
    with timeout(1.5) as t:

        await asyncio.sleep(1)  # first

        await asyncio.sleep(1)  # second

        await asyncio.sleep(1)  # third

        print('not timeout')


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

输出:

Traceback (most recent call last):
  File "C:\Users\gmn\main.py", line 74, in main
    await asyncio.sleep(1)  # second
  File "C:\Users\gmn\AppData\Local\Programs\Python\Python37\lib\asyncio\tasks.py", line 564, in sleep
    return await future
concurrent.futures._base.CancelledError

During handling of the above exception, another exception occurred:
  ...

这里的第二行和第三行告诉你发生超时的地方:

  File "C:\Users\gmn\main.py", line 74, in main
    await asyncio.sleep(1)  # second

【讨论】:

  • async_timeout 是一个不错的模块,但它并没有解决 OP 的问题。该问题清楚地表明“两个操作都需要一个超时时间,并且 - 这很重要 - 带有一条错误消息,告诉您哪个操作超时了。”无论哪个操作超时,async_timeout 都会引发完全相同的错误。
  • @user4815162342 不,错误不是“完全相同”。错误具有相同的类型和消息,但不同的回溯“告诉哪个操作超时”。它可以清楚地识别超时发生的位置。如果我理解正确,它可以完全解决 OP 的问题。
  • OP 指定他们想要一个“错误消息告诉哪个操作超时”。在一般的 Python 中,特别是在 OP 的代码中,错误消息来自异常对象,而不是人们必须从回溯中猜测的东西。回溯是一个很好的调试功能,但当行恰好相同时,或者当程序未附带.py 源时,它就无济于事了。此外,要提取实际消息,必须解析回溯。
  • @user4815162342 好吧,我想我的答案的实际情况取决于OP到底需要什么:控制台中的用户消息或开发人员的调试信息(我稍后假设)。另外我想指出,如果行恰好相同,回溯仍然会有所帮助:它包含行号。
  • 作为 OP,首先我要感谢有关 async_timeout 库的信息。我学到了一些新东西。关于我的需求,我更喜欢日志的清晰人类可读信息,我可以自己制定。这就是为什么我认为这个库不适合我正在编程的工具。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-31
  • 1970-01-01
  • 1970-01-01
  • 2020-10-30
  • 1970-01-01
相关资源
最近更新 更多