在内存和速度方面,任何asyncio任务的开销是多少?在不需要并发运行的情况下,减少任务数量是否值得考虑?
在内存和速度方面,任何asyncio任务的开销是多少?在不需要并发运行的情况下,减少任务数量是否值得考虑?
await some_coro() # (1)
await asyncio.create_task(some_coro()) # (2)
asyncio.gather
或asyncio.wait_for
),创建不必要的任务很容易出现。(我怀疑这个问题背后建立或使用了这样的抽象。)async def noop():
pass
async def mem1():
tasks = [asyncio.create_task(noop()) for _ in range(1000000)]
time.sleep(60) # not asyncio.sleep() in this case - we don't
# want our noop tasks to exit immediately
async def mem2():
coros = [noop() for _ in range(1000000)]
time.sleep(60)
async def cpu1():
t0 = time.time()
for _ in range(1000000):
await asyncio.create_task(noop())
t1 = time.time()
print(t1-t0)
在我的电脑上,平均需要27秒才能运行(波动很小)。没有任务的版本看起来像这样:
async def cpu2():
t0 = time.time()
for _ in range(1000000):
await noop()
t1 = time.time()
print(t1-t0)
这个只需要0.16秒,相当于 ~170 的因子!所以结果表明,与等待协程对象相比,等待任务的时间开销是不可忽略的。这是由于两个原因:
与协程对象相比,任务的创建成本更高,因为它们需要初始化基本Future
,然后是Task
本身的属性,并最终将任务插入事件循环中,带有自己的簿记。
新创建的任务处于挂起状态,其构造函数scheduled在第一次机会时开始执行协程。由于任务拥有协程对象,因此等待新任务不能立即开始执行协程;它必须暂停并等待任务开始执行它。即使等待不挂起的协程,等待的协程也只会在完整的事件循环迭代后恢复!事件循环迭代代价高昂,因为它要遍历所有可运行的任务和轮询内核以获取IO和超时活动。事实上,cpu1
的strace
显示了两百万次对epoll_wait(2)
的调用。另一方面,cpu2
仅偶尔向内核请求与分配相关的mmap()
,总共只有几千个。
相比之下,直接等待协程doesn't yield到事件循环,除非被等待的协程本身decides挂起。相反,它会立即开始执行协程,就像它是一个普通函数。
coros = [noop() for _ in range(1000000)]
是否会安排所有 noop
运行? - Michal CharemzaTask
的属性,而不是低级协程对象的属性。在内存基准测试中,创建一百万个协程只是为了使内存使用量显现出来,并没有假设实际等待它们的运行时语义是相同的。 - user4815162342async def noop():asyncio.sleep(0)
,则会得到10秒
而不是30秒
。我不确定我是否同意“协程足够简单”的论点:如果它不需要挂起,特别是数百万个,那么就没有必要创建协程。还是感谢您的研究! - Mikhail Gerasimovstream.read()
作为一个例子,它的工作方式正是如此,但还有其他例子,比如queue.get
和queue.put
,许多异步上下文管理器的__aenter__
方法,非争用情况下的同步方法等等。有许多低级协程在等待时不会每次都挂起。 - user4815162342await
只需1次事件循环迭代就能完成所有工作。所以即使是 @MikhailGerasimov 的10秒
与30秒
也是错误的,因为它将其分布在100万次事件循环迭代与300万次事件循环迭代之间。 - Philip CoulingTask
本身只是一个微小的Python对象。它需要极少量的内存和CPU。然而,由Task
运行的操作(通常运行协程Task)可能会消耗自己的资源,例如:
(*) 除非你正在进行非常特殊的操作并计划创建数十亿个任务。在这种情况下,您应该使用Queue或类似方法懒惰地创建它们。