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



我有一个具有各种方法的类。我在那个类中有一个方法类似于:

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"  

这里的问题是:

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

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

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


为什么是gather?

gather在退出协程之前积累子任务的目的是延迟协程的完成,直到它的子任务完成。这个封装了实现,并确保协程作为一个单独的实体"做它的事情"。
另一种方法是return子任务,并期望调用者运行它们直到完成。

为简单起见,让我们看一个单层——对应于中间的sub_method——但有不同的变化。

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

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

  • encapsulated是"最重的"。变体:所有子节点都在Task中运行,有一个额外的gather。然而,调用者不暴露于任何这些:
    await encapsulated()
    
    这保证了功能按预期工作,并且可以自由地更改其实现。
  • task_children是中间变量:所有子变量都在Tasks中运行。调用者可以决定是否以及如何等待完成:
    tasks = await task_children()
    await asyncio.gather(*tasks)  # can add other tasks here as well
    
    这保证了功能按预期启动。它的完成依赖于调用者有一些知识。
  • coro_children是"最轻的"。变体:实际上没有运行子进程。调用者负责整个生命周期:
    tasks = await coro_children()
    # children don't actually run yet!
    await asyncio.gather(*tasks)  # can add other tasks here as well
    
    这完全依赖于调用者启动并等待子任务。

使用encapsulated模式是一个安全的默认设置——它确保协程"正常工作"。值得注意的是,使用内部gather的协程看起来仍然和其他协程一样。

gather速度?

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

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

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

master_method                                                  0
sub_method         0          1          2          3          4          5 ...
my_task       0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 ...

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

相关内容

  • 没有找到相关文章

最新更新