任何 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 本身的属性,最后将任务插入事件循环,并带有自己的簿记.
-
一个新创建的任务处于挂起状态,它的构造函数有scheduled它在第一时间开始执行协程。由于任务拥有协程对象,等待新任务不能只是开始执行协程;它必须暂停并等待任务开始执行它。等待的协程只会在完整的事件循环迭代后恢复,即使在等待选择不暂停的协程时也是如此!事件循环迭代代价高昂,因为它会遍历所有可运行的任务并轮询内核以获取 IO 和超时活动。事实上,strace 中的 cpu1 显示有 200 万次调用 epoll_wait(2)。另一方面,cpu2 仅用于偶尔与分配相关的mmap() 内核,总共有几千个。
相反,除非等待的协程本身决定暂停,否则直接等待协程doesn't yield 到事件循环。相反,它会立即开始执行协程,就好像它是一个普通函数一样。
因此,如果您的协程的快乐路径不涉及挂起(例如非竞争同步原语或从有数据要提供的非阻塞套接字读取流的情况),等待它的成本相当于函数调用的成本。这比等待任务所需的事件循环迭代要快得多,并且在延迟很重要时会有所作为。