我有一个具有各种方法的类。我在那个类中有一个方法类似于:
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"
这里的问题是:
在从另一个
asyncio.gather()
调用的协同例程中使用asyncio.gather()
是否有任何问题,优点/缺点?有性能问题吗?所有级别的所有任务都被
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_children
和coro_children
都以某种方式编码了子任务。这允许调用者以"完成"实际目标的方式运行它们;可靠的。然而,每个变体的不同之处在于它自己做了多少事情,以及调用者必须做多少事情:
encapsulated
是"最重的"。变体:所有子节点都在Task
中运行,和有一个额外的gather
。然而,调用者不暴露于任何这些:
这保证了功能按预期工作,并且可以自由地更改其实现。await encapsulated()
task_children
是中间变量:所有子变量都在Task
s中运行。调用者可以决定是否以及如何等待完成:
这保证了功能按预期启动。它的完成依赖于调用者有一些知识。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)确保其参数作为Task
s运行,b)提供一旦任务完成就触发的Future
。由于gather
通常在作为Task
s运行参数时使用,因此没有额外的开销;同样,这些都是常规的Task
s,并且具有与其他所有内容相同的性能/优先级特征¹。
唯一的开销来自包装Future
;它负责记账(确保参数是任务),然后只等待,即什么都不做。在我的机器上,测量开销显示,它平均花费的时间大约是运行无操作Task
的两倍。对于任何现实世界的任务来说,这本身应该是可以忽略不计的。
此外,gather
处理子任务的模式本质上意味着存在gather
节点的树。因此,gather
节点的数量通常远低于任务的数量。例如,在每个gather
有10个任务的情况下,总共只需要11个gather
s来处理总共100个任务。
master_method 0
sub_method 0 1 2 3 4 5 ...
my_task 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 ...
¹也就是说,没有。asyncio
目前没有Task
优先级的概念。