【问题标题】:Asyncio.gather vs asyncio.waitAsyncio.gather 与 asyncio.wait
【发布时间】:2017-07-03 00:55:29
【问题描述】:

asyncio.gatherasyncio.wait 似乎有相似的用途:我有一堆我想要执行/等待的异步事情(不一定要在下一个开始之前等待一个完成)。它们使用不同的语法,并且在某些细节上有所不同,但对我来说,拥有两个在功能上具有如此巨大重叠的函数似乎非常不符合 Python 标准。我错过了什么?

【问题讨论】:

    标签: python asynchronous async-await python-asyncio


    【解决方案1】:

    当涉及到异常时,很容易忽略的一个非常重要的区别是这两个函数的默认行为。


    我将使用这个例子来模拟一个有时会引发异常的协程 -

    import asyncio
    import random
    
    
    async def a_flaky_tsk(i):
        await asyncio.sleep(i)  # bit of fuzz to simulate a real-world example
    
        if i % 2 == 0:
            print(i, "ok")
        else:
            print(i, "crashed!")
            raise ValueError
    
    coros = [a_flaky_tsk(i) for i in range(10)]
    

    await asyncio.gather(*coros) 输出 -

    0 ok
    1 crashed!
    Traceback (most recent call last):
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 20, in <module>
        asyncio.run(main())
      File "/Users/dev/.pyenv/versions/3.8.2/lib/python3.8/asyncio/runners.py", line 43, in run
        return loop.run_until_complete(main)
      File "/Users/dev/.pyenv/versions/3.8.2/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
        return future.result()
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 17, in main
        await asyncio.gather(*coros)
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
        raise ValueError
    ValueError
    

    如您所见,索引 1 之后的 coros 永远不会执行。


    但是await asyncio.wait(coros) 继续执行任务,即使其中一些任务失败 -

    0 ok
    1 crashed!
    2 ok
    3 crashed!
    4 ok
    5 crashed!
    6 ok
    7 crashed!
    8 ok
    9 crashed!
    Task exception was never retrieved
    future: <Task finished name='Task-10' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
    Traceback (most recent call last):
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
        raise ValueError
    ValueError
    Task exception was never retrieved
    future: <Task finished name='Task-8' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
    Traceback (most recent call last):
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
        raise ValueError
    ValueError
    Task exception was never retrieved
    future: <Task finished name='Task-2' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
    Traceback (most recent call last):
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
        raise ValueError
    ValueError
    Task exception was never retrieved
    future: <Task finished name='Task-9' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
    Traceback (most recent call last):
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
        raise ValueError
    ValueError
    Task exception was never retrieved
    future: <Task finished name='Task-3' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
    Traceback (most recent call last):
      File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
        raise ValueError
    ValueError
    

    当然,这两种行为都可以通过使用 -

    来改变

    asyncio.gather(..., return_exceptions=True)

    或者,

    asyncio.wait([...], return_when=asyncio.FIRST_EXCEPTION)


    但它并没有到此结束!

    注意: Task exception was never retrieved 在上面的日志中。

    asyncio.wait() 不会重新引发子任务的异常,直到您单独 await 它们。 (日志中的堆栈跟踪只是消息,它们无法被捕获!)

    done, pending = await asyncio.wait(coros)
    for tsk in done:
        try:
            await tsk
        except Exception as e:
            print("I caught:", repr(e))
    

    输出 -

    0 ok
    1 crashed!
    2 ok
    3 crashed!
    4 ok
    5 crashed!
    6 ok
    7 crashed!
    8 ok
    9 crashed!
    I caught: ValueError()
    I caught: ValueError()
    I caught: ValueError()
    I caught: ValueError()
    I caught: ValueError()
    

    另一方面,要使用asyncio.gather() 捕获异常,您必须 -

    results = await asyncio.gather(*coros, return_exceptions=True)
    for result_or_exc in results:
        if isinstance(result_or_exc, Exception):
            print("I caught:", repr(result_or_exc))
    

    (与之前相同的输出)

    【讨论】:

    • 直到看到这篇文章,我才明白Task exception was never retrieved 错误。非常感谢您的精彩解释..
    【解决方案2】:

    除了之前所有的答案,我想告诉你gather()wait() 的不同行为,以防它们被取消

    Gather取消

    如果gather() 被取消,所有已提交的等待项(尚未完成)也将被取消

    Wait取消

    如果wait() 任务被取消,它只会抛出一个CancelledError 并且等待的任务保持不变。

    简单示例:

    import asyncio
    
    
    async def task(arg):
        await asyncio.sleep(5)
        return arg
    
    
    async def cancel_waiting_task(work_task, waiting_task):
        await asyncio.sleep(2)
        waiting_task.cancel()
        try:
            await waiting_task
            print("Waiting done")
        except asyncio.CancelledError:
            print("Waiting task cancelled")
    
        try:
            res = await work_task
            print(f"Work result: {res}")
        except asyncio.CancelledError:
            print("Work task cancelled")
    
    
    async def main():
        work_task = asyncio.create_task(task("done"))
        waiting = asyncio.create_task(asyncio.wait({work_task}))
        await cancel_waiting_task(work_task, waiting)
    
        work_task = asyncio.create_task(task("done"))
        waiting = asyncio.gather(work_task)
        await cancel_waiting_task(work_task, waiting)
    
    
    asyncio.run(main())
    

    输出:

    asyncio.wait()
    Waiting task cancelled
    Work result: done
    ----------------
    asyncio.gather()
    Waiting task cancelled
    Work task cancelled
    

    有时需要结合wait()gather() 功能。例如,我们希望等待至少一个任务完成,然后取消其余待处理的任务,如果waiting本身被取消,然后同时取消所有待处理的任务。

    作为真实示例,假设我们有一个断开连接事件和一个工作任务。而我们想要等待工作任务的结果,但是如果连接丢失了,那么就取消它。或者我们将发出多个并行请求,但在完成至少一个响应后,取消所有其他响应。

    可以这样做:

    import asyncio
    from typing import Optional, Tuple, Set
    
    
    async def wait_any(
            tasks: Set[asyncio.Future], *, timeout: Optional[int] = None,
    ) -> Tuple[Set[asyncio.Future], Set[asyncio.Future]]:
        tasks_to_cancel: Set[asyncio.Future] = set()
        try:
            done, tasks_to_cancel = await asyncio.wait(
                tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
            )
            return done, tasks_to_cancel
        except asyncio.CancelledError:
            tasks_to_cancel = tasks
            raise
        finally:
            for task in tasks_to_cancel:
                task.cancel()
    
    
    async def task():
        await asyncio.sleep(5)
    
    
    async def cancel_waiting_task(work_task, waiting_task):
        await asyncio.sleep(2)
        waiting_task.cancel()
        try:
            await waiting_task
            print("Waiting done")
        except asyncio.CancelledError:
            print("Waiting task cancelled")
    
        try:
            res = await work_task
            print(f"Work result: {res}")
        except asyncio.CancelledError:
            print("Work task cancelled")
    
    
    async def check_tasks(waiting_task, working_task, waiting_conn_lost_task):
        try:
            await waiting_task
            print("waiting is done")
        except asyncio.CancelledError:
            print("waiting is cancelled")
    
        try:
            await waiting_conn_lost_task
            print("connection is lost")
        except asyncio.CancelledError:
            print("waiting connection lost is cancelled")
    
        try:
            await working_task
            print("work is done")
        except asyncio.CancelledError:
            print("work is cancelled")
    
    
    async def work_done_case():
        working_task = asyncio.create_task(task())
        connection_lost_event = asyncio.Event()
        waiting_conn_lost_task = asyncio.create_task(connection_lost_event.wait())
        waiting_task = asyncio.create_task(wait_any({working_task, waiting_conn_lost_task}))
        await check_tasks(waiting_task, working_task, waiting_conn_lost_task)
    
    
    async def conn_lost_case():
        working_task = asyncio.create_task(task())
        connection_lost_event = asyncio.Event()
        waiting_conn_lost_task = asyncio.create_task(connection_lost_event.wait())
        waiting_task = asyncio.create_task(wait_any({working_task, waiting_conn_lost_task}))
        await asyncio.sleep(2)
        connection_lost_event.set()  # <---
        await check_tasks(waiting_task, working_task, waiting_conn_lost_task)
    
    
    async def cancel_waiting_case():
        working_task = asyncio.create_task(task())
        connection_lost_event = asyncio.Event()
        waiting_conn_lost_task = asyncio.create_task(connection_lost_event.wait())
        waiting_task = asyncio.create_task(wait_any({working_task, waiting_conn_lost_task}))
        await asyncio.sleep(2)
        waiting_task.cancel()  # <---
        await check_tasks(waiting_task, working_task, waiting_conn_lost_task)
    
    
    async def main():
        print("Work done")
        print("-------------------")
        await work_done_case()
        print("\nConnection lost")
        print("-------------------")
        await conn_lost_case()
        print("\nCancel waiting")
        print("-------------------")
        await cancel_waiting_case()
    
    
    asyncio.run(main())
    

    输出:

    Work done
    -------------------
    waiting is done
    waiting connection lost is cancelled
    work is done
    
    Connection lost
    -------------------
    waiting is done
    connection is lost
    work is cancelled
    
    Cancel waiting
    -------------------
    waiting is cancelled
    waiting connection lost is cancelled
    work is cancelled
    

    【讨论】:

      【解决方案3】:

      asyncio.wait 的级别比asyncio.gather 低。

      顾名思义,asyncio.gather 主要侧重于收集结果。它等待一堆期货并以给定的顺序返回它们的结果。

      asyncio.wait 只是等待期货。而不是直接给你结果,它给出了已完成和待处理的任务。您必须手动收集这些值。

      此外,您可以使用wait 指定等待所有期货完成或仅等待第一个期货完成。

      【讨论】:

      • @Kingname ..wat
      • 您的意思是 asyncio.gather 必须等待所有这些都完成,而 asyncio.wait 将返回给您每个的当前状态(未决与否)?读你的答案我不清楚
      • @EigenFool 从 Python 3.9 开始,asyncio.wait 有一个名为 return_when 的参数,您可以使用它来控制事件循环何时应让步给您。 asyncio.gather 没有这样的参数,事件循环只有在所有任务完成/失败时才会回复你。在此处阅读官方文档:docs.python.org/3/library/asyncio-task.html#asyncio.wait
      • @ospider 用于asyncio.wait 的参数名为return_when 已在Python 3.5.9 中可用!见这里:docs.python.org/3.5/library/asyncio-task.html#asyncio.wait
      • @Kingname python -m timeit "print('hello')" 为每个循环提供 36.6 微秒,因此 10000000000000 print('hello') 将需要 11.6 年才能完成 print() 函数
      【解决方案4】:

      我还注意到,您可以通过简单地指定列表来在 wait() 中提供一组协程:

      result=loop.run_until_complete(asyncio.wait([
              say('first hello', 2),
              say('second hello', 1),
              say('third hello', 4)
          ]))
      

      而在gather() 中的分组是通过指定多个协程来完成的:

      result=loop.run_until_complete(asyncio.gather(
              say('first hello', 2),
              say('second hello', 1),
              say('third hello', 4)
          ))
      

      【讨论】:

      • 列表也可以与gather()一起使用,例如:asyncio.gather(*task_list)
      • 发电机也可以
      • 如何在不阻塞脚本其余部分的情况下使用此集合?
      • 太棒了。感谢您提供更易于阅读的示例。
      • @thebeancounter 你不需要马上awaitgroup = asyncio.gather(*aws) 直接返回组的等待/未来,它代表所有组合任务。任务可以在asyncio.gather-call 之后很快运行,例如当有一个await 用于其他东西(如asyncio.sleep)或访问未来时(如group.done())。您只需要使用await group,当您想确保任务完成或取消并收集所有结果时。
      【解决方案5】:

      虽然在一般情况下类似(“运行并获取许多任务的结果”),但每个函数都有一些针对其他情况的特定功能:

      asyncio.gather()

      返回一个 Future 实例,允许对任务进行高级分组:

      import asyncio
      from pprint import pprint
      
      import random
      
      
      async def coro(tag):
          print(">", tag)
          await asyncio.sleep(random.uniform(1, 3))
          print("<", tag)
          return tag
      
      
      loop = asyncio.get_event_loop()
      
      group1 = asyncio.gather(*[coro("group 1.{}".format(i)) for i in range(1, 6)])
      group2 = asyncio.gather(*[coro("group 2.{}".format(i)) for i in range(1, 4)])
      group3 = asyncio.gather(*[coro("group 3.{}".format(i)) for i in range(1, 10)])
      
      all_groups = asyncio.gather(group1, group2, group3)
      
      results = loop.run_until_complete(all_groups)
      
      loop.close()
      
      pprint(results)
      

      可以通过调用group2.cancel() 甚至all_groups.cancel() 来取消组中的所有任务。另见.gather(..., return_exceptions=True)

      asyncio.wait()

      支持在第一个任务完成后或指定超时后等待停止,允许较低级别的操作精度:

      import asyncio
      import random
      
      
      async def coro(tag):
          print(">", tag)
          await asyncio.sleep(random.uniform(0.5, 5))
          print("<", tag)
          return tag
      
      
      loop = asyncio.get_event_loop()
      
      tasks = [coro(i) for i in range(1, 11)]
      
      print("Get first result:")
      finished, unfinished = loop.run_until_complete(
          asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED))
      
      for task in finished:
          print(task.result())
      print("unfinished:", len(unfinished))
      
      print("Get more results in 2 seconds:")
      finished2, unfinished2 = loop.run_until_complete(
          asyncio.wait(unfinished, timeout=2))
      
      for task in finished2:
          print(task.result())
      print("unfinished2:", len(unfinished2))
      
      print("Get all other results:")
      finished3, unfinished3 = loop.run_until_complete(asyncio.wait(unfinished2))
      
      for task in finished3:
          print(task.result())
      
      loop.close()
      

      【讨论】:

      • "单星号形式 ( *args ) 用于传递无关键字的可变长度参数列表,双星号形式用于传递带关键字的可变长度参数列表"
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-12-09
      • 1970-01-01
      • 1970-01-01
      • 2016-02-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多