异步任务的开销是多少?

问题描述

任何异步任务在内存和速度方面的开销是多少?在不需要并发运行的情况下将任务数降至最低是否值得?


解决方案

任何异步任务在内存和速度方面的开销是多少?

TL;DR内存开销看起来可以忽略不计,但时间开销可能很大,特别是在等待的协程选择不挂起的情况下。

让我们假设您正在测量与直接等待的协程相比的任务开销,例如:

await some_coro()                       # (1)
await asyncio.create_task(some_coro())  # (2)

没有理由直接编写(2),但是当使用自动"futurize"接收到的等待项的接口(如asyncio.gatherasyncio.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

在我的运行Python3.7的64位Linux机器上,该进程大约消耗1GiB的内存。这大约是每个任务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和超时活动。实际上,cpu1strace显示了对epoll_wait(2)的200万次调用。cpu2另一方面,只有零星的与分配相关的mmap()进入内核,总共几千个。

    相反,直接等待协同例程doesn't yield到事件循环,除非等待的协同例程本身决定暂停。相反,它会立即继续并开始执行协同例程,就好像它是一个普通函数一样。

    /li>
因此,如果协程的快乐路径不涉及挂起(就像非争用同步原语的情况一样,或者从有数据要提供的非阻塞套接字读取流的情况),等待它的成本与函数调用的成本相当。这比等待任务所需的事件循环迭代快得多,而且在延迟很重要的时候可以发挥作用。

相关文章