简体   繁体   English

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

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

In python > 3.5, how can I have a coroutine return a final value after being cancelled due to TimeoutError?在python> 3.5中,如何让协程在因TimeoutError被取消后返回最终值?

I have a small python project that uses multiple coroutines to transfer data and reports back the amount of data transferred.我有一个小型 python 项目,它使用多个协程来传输数据并报告传输的数据量。 It takes a timeout parameter;它需要一个超时参数; if the script times out prior to completing the transfer, it reports back the amount that it transferred prior to cancellation.如果脚本在完成转移之前超时,它会报告取消前转移的金额。

It was working fine in python3.5, but recently I tried updating to 3.8 and ran into trouble.它在 python3.5 中运行良好,但最近我尝试更新到 3.8 并遇到了麻烦。

Below is example code, and clearly its behavior differs widely from 3.5, 3.6, 3.7, and 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__: is not printing for 3.8 because CancelledError is now BaseException instead of Exception (EDIT: which may be why the traceback prints here but not elsewhere). exception in __main__:不为 3.8 打印,因为CancelledError现在是BaseException而不是Exception (编辑:这可能是回溯打印在这里而不是其他地方的原因)。

I have tried a number of configurations of using return_exceptions=True in asyncio.gather or catching CancelledError in the except asyncio.TimeoutError: block, but I can't seem to get it right.我已经尝试了许多在asyncio.gather中使用return_exceptions=True或在except asyncio.TimeoutError:块中捕获CancelledError的配置,但我似乎无法做到正确。

I need to keep main as an async function, because in my actual code it is creating an aiohttp Session for the other coroutines to share, and modern aiohttp requires this to be done in an async contextmanager (instead of a regular sync context manager).我需要将main作为异步函数保留,因为在我的实际代码中,它正在为其他协程共享创建 aiohttp 会话,而现代 aiohttp 要求在异步上下文管理器(而不是常规同步上下文管理器)中完成此操作。

I'm hoping for code that runs on 3.5-3.8, so I'm not using asyncio.run .我希望在 3.5-3.8 上运行的代码,所以我没有使用asyncio.run

I've tried code from a number of other questions that use .cancel() with or without contextlib.suppress(asyncio.CancelledError) , but still no luck.我已经尝试了许多其他问题的代码,这些问题使用.cancel()有或没有contextlib.suppress(asyncio.CancelledError) ,但仍然没有运气。 I've also tried returning an awaited value (eg result = await coros; return result instead of return coros.result() ), also no dice.我也试过返回一个等待的值(例如result = await coros; return result而不是return coros.result() ),也没有骰子。

Is there a good way for me to get the python 3.5 behavior in python >3.5, in which I can have a coroutine catch CancelledError on timeout and return a value when next awaited?有没有一种好方法可以让我在 python > 3.5 中获得 python 3.5 的行为,其中我可以让协程在超时时捕获CancelledError并在下一次等待时返回一个值?

Thanks in advance.提前致谢。

I did some debugging and it looks like the result is never set in case of asyncio.gather cancellation so there's no way to retrieve it from _GatheringFuture object in python 3.8.我做了一些调试,看起来结果在 asyncio.gather 取消的情况下从未设置过,因此无法从 python 3.8 中的_GatheringFuture对象中检索它。

asyncio/tasks.py:792异步/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)

Reading through documentation I found this about asyncio.CancelledError : asyncio.CancelledError文档我发现这个关于asyncio.CancelledError

In almost all situations the exception must be re-raised.在几乎所有情况下,都必须重新引发异常。

Imo, the python 3.5 behavior was rather unintentional. Imo,python 3.5 的行为是无意的。 I wouldn't rely on it.我不会依赖它。

While it may be possible to work around this by not using asyncio.gather , it's not worth the effort.虽然可以通过不使用asyncio.gather来解决这个asyncio.gather ,但这样做并不值得。 If you really need to get the partial result from a cancelled coroutine, then just add it to some global list:如果您确实需要从取消的协程中获取部分结果,那么只需将其添加到某个全局列表中:

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

Thanks to @RafalS and their suggestion to stop using asyncio.gather .感谢@RafalS 和他们建议停止使用asyncio.gather

Instead of using gather and wait_for , it seems that using the timeout from .wait directly with the coroutines may be the best bet, and works from 3.5 to 3.8.而不是使用gatherwait_for ,似乎将.wait的超时直接与协程一起使用可能是最好的选择,并且适用于 3.5 到 3.8。

Note that the bash command below is slightly modified to show that the tasks are being run simultaneously and also being cancelled without waiting for foo to complete.请注意,下面的 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