在内存和速度方面,任何asyncio任务的开销是多少?在不需要同时运行的情况下,是否值得减少任务数量?
在内存和速度方面,任何asyncio任务的开销是多少?
TL; DR内存开销可以忽略不计,但是时间开销可能很大,特别是当等待的协同程序选择不挂起时。
让我们假设您正在测量任务的开销与直接等待的协程相比,例如:
await some_coro() # (1)
await asyncio.create_task(some_coro()) # (2)
没有理由直接写(2),但是当使用自动"futurize"他们收到的等待的API时,很容易产生不必要的任务,例如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
在我运行Python 3.7的64位Linux机器上,该进程消耗大约1 GiB的内存。这大约是每个任务1个KiB +协程,并且它计算任务的内存和事件循环簿记中的条目的内存。以下程序测量一个协程开销的近似值:
async def mem2():
coros = [noop() for _ in range(1000000)]
time.sleep(60)
上述过程大约需要550 MiB的内存,或者每个协程只需0.55 KiB。所以看起来虽然任务不是完全免费的,但它并没有对协程施加巨大的内存开销,特别是要记住上面的协程是空的。如果协程具有某种状态,则开销会小得多(相对而言)。
但是CPU开销怎么样 - 与等待协程相比,创建和等待任务需要多长时间?我们来试试一个简单的测量:
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
本身的属性,最后将任务插入到事件循环中,并使用自己的簿记。strace
的cpu1
显示了两百万次对epoll_wait(2)
的召唤。另一方面,cpu2
仅用于偶尔与分配相关的mmap()
的核心,总共几千。
相反,直接等待协程doesn't yield到事件循环,除非等待的协程本身决定暂停。相反,它立即继续并开始执行协程,就好像它是一个普通的函数。因此,如果您的协程的快乐路径不涉及挂起(如非竞争同步原语或从具有要提供的数据的非阻塞套接字读取流的情况),等待它的成本与成本相当一个函数调用。这比等待任务所需的事件循环迭代要快得多,并且在延迟很重要时可以有所作为。
Task
本身只是一个小小的Python对象。它需要可怜的内存和CPU。另一方面,由Task
(Task通常运行协程)运行的操作可能会消耗其自身的显着资源,例如:
通常(*)您不必以相同的方式考虑任务数量,例如,您通常不会考虑Python脚本中的函数调用数。
但是,当然你应该总是考虑你的异步程序如何工作。如果要同时发出很多I / O请求或产生很多同时发生的线程/进程,你应该使用Semaphore来避免同时获取太多资源。
(*)除非你正在做一些非常特别的事情,并计划创造数十亿的任务。在这种情况下,你应该使用Queue或类似的东西懒洋洋地创建它们。