在另一个 asyncio.gather() 中使用嵌套的 asyncio.gather()

KZi*_*vas 8 python event-loop coroutine python-asyncio

我有一堂有各种方法的课。我在该类中有一个类似的方法:

 class MyClass:

    async def master_method(self):
      tasks = [self.sub_method() for _ in range(10)]
      results = await asyncio.gather(*tasks)

    async def sub_method(self):
      subtasks = [self.my_task() for _ in range(10)]
      results = await asyncio.gather(*subtasks)

    async def my_task(self):
      return "task done"  
Run Code Online (Sandbox Code Playgroud)

所以这里的问题是:

  1. 使用asyncio.gather()从另一个程序调用的内部协同例程是否存在任何问题、优点/缺点asyncio.gather()?有性能问题吗?

  2. 循环中所有级别的所有任务是否都以相同的优先级处理asyncio?这是否会提供与我使用单个 asyncio.gather() 调用所有协同例程相同的性能master_method

Mis*_*agi 8

TLDR:使用gather而不是返回任务可以简化使用并使代码更易于维护。虽然gather有一些开销,但对于任何实际应用来说都是可以忽略不计的。

\n
\n

为什么gather

\n

在退出协程之前累积子任务的目的gather是延迟协程的完成,直到其子任务完成。这封装了实现,并确保协程显示为一个“做它的事情”的单个实体。
\n另一种选择是return子任务,并期望调用者运行它们直至完成。

\n

为简单起见,让我们看一下与中间 \xe2\x80\x93 相对应的单层sub_method\xe2\x80\x93,但具有不同的变体。

\n
async def child(i):\n    await asyncio.sleep(0.2)  # some non-trivial payload\n    print("child", i, "done")\n\nasync def encapsulated() -> None:\n    await asyncio.sleep(0.1)  # some preparation work\n    children = [child() for _ in range(10)]\n    await asyncio.gather(*children)\n\nasync def task_children() -> \'List[asyncio.Task]\':\n    await asyncio.sleep(0.1)  # some preparation work\n    children = [asyncio.create_task(child()) for _ in range(10)]\n    return children\n\nasync def coro_children() -> \'List[Awaitable[None]]\':\n    await asyncio.sleep(0.1)  # some preparation work\n    children = [child() for _ in range(10)]\n    return children\n
Run Code Online (Sandbox Code Playgroud)\n

所有encapsulated,task_childrencoro_children以某种方式编码存在子任务。这允许调用者以可靠地“完成”实际目标的方式运行它们。然而,每个变体的不同之处在于它本身做了多少事情以及调用者必须做多少事情:

\n
    \n
  • 这是“最重”的变体:所有子项都在sencapsulated中运行,并且还有一个附加的. 但是,调用者不会遇到以下任何情况:\nTaskgather
    await encapsulated()\n
    Run Code Online (Sandbox Code Playgroud)\n这保证了功能按预期工作,并且可以自由更改其实现。
  • \n
  • task_children是中间变体:所有子项都在Tasks 中运行。调用者可以决定是否以及如何等待完成:\n
    tasks = await task_children()\nawait asyncio.gather(*tasks)  # can add other tasks here as well\n
    Run Code Online (Sandbox Code Playgroud)\n这保证了功能按预期启动。不过,它的完成依赖于调用者是否具备一定的知识。
  • \n
  • coro_children是“最轻”的变体:实际上没有任何子项运行。调用者对整个生命周期负责:\n
    tasks = await coro_children()\n# children don\'t actually run yet!\nawait asyncio.gather(*tasks)  # can add other tasks here as well\n
    Run Code Online (Sandbox Code Playgroud)\n这完全依赖调用者来启动并等待子任务。
  • \n
\n

使用该encapsulated模式是安全的默认 \xe2\x80\x93 它确保协程“正常工作”。值得注意的是,使用内部的 协程gather看起来仍然与任何其他协程一样。

\n

gather速度?

\n

实用程序a) 确保其参数作为sgather运行,b) 提供 a ,一旦任务完成就会触发。由于通常在将参数作为s 运行时使用,因此不会产生额外的开销;同样,这些都是常规的,并且具有与其他所有内容相同的性能/优先级特征\xc2\xb9。TaskFuturegatherTaskTask

\n

唯一的开销来自于包装Future;这负责簿记(确保参数是任务),然后只等待,即什么都不做。在我的机器上,测量开销表明,它平均花费的时间大约是运行 no-op 的两倍Task。对于任何现实世界的任务来说,这本身应该可以忽略不计。

\n

此外,gathering 子任务的模式本质上意味着存在一棵节点gather。因此,节点的数量gather通常远低于任务的数量。例如,对于每个 10 个任务的情况gather,总共只gather需要 11 秒即可处理总共 100 个任务。

\n
master_method                                                  0\n\nsub_method         0          1          2          3          4          5 ...\n\nmy_task       0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 ...\n
Run Code Online (Sandbox Code Playgroud)\n
\n

\xc2\xb9也就是说,没有。asyncio目前没有Task优先级的概念。

\n