异步任务的开销是多少?

时间:2019-04-19 12:11:43

标签: python python-3.x python-asyncio event-loop

就内存和速度而言,任何异步任务的开销是多少?在不需要并发运行的情况下,是否有必要将任务数量减到最少?

2 个答案:

答案 0 :(得分:3)

  

就内存和速度而言,任何异步任务的开销是多少?

TL; DR 内存开销似乎可以忽略不计,但是时间开销可能很大,尤其是在等待的协程选择不暂停时。

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

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

没有理由直接写(2),但是当使用自动"futurize"收到asyncio.gatherasyncio.wait_for之类的API时,创建不必要的任务很容易。 (我怀疑构建或使用这种抽象是该问题的背景。)

可以直接测量两个变量之间的内存和时间差。例如,以下程序创建了一百万个任务,并且该进程的内存消耗可以除以一百万以估算任务的内存成本:

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显示了对epoll_wait(2)的200万次呼叫。另一方面,cpu2仅与偶尔与分配相关的mmap()一起进入内核,总数为数千。

    相反,除非等待的协程本身决定,否则直接在事件循环中等待协程doesn't yield。相反,它立即继续运行并开始执行协程,就好像它是普通函数一样。

因此,如果协程的快乐路径不涉及挂起(例如非竞争性同步原语或从具有提供数据的非阻塞套接字读取流的情况),则等待它的成本可与函数调用的成本。这比等待任务所需的事件循环迭代要快得多,并且在等待时间很重要时可以有所作为。

答案 1 :(得分:1)

Task本身只是一个很小的Python对象。它需要大量的内存和CPU。另一方面,由TaskTask通常运行协程)运行的操作可能会消耗其自己的明显资源,例如:

  • 如果我们谈论网络操作(网络读/写),则为网络带宽
  • CPU /内存,如果我们谈论使用run_in_executor在单独的进程中运行操作

通常(*),您不必以相同的方式来考虑任务的数量,例如,您通常不必考虑Python脚本中的函数调用的数量。

但是,当然,您应该总考虑一下异步程序的总体工作原理。如果要发出大量并发的I / O请求或产生大量并发的线程/进程,则应使用Semaphore以避免同时获取太多资源。


(*),除非您正在做非常特别的事情并计划创建数十亿个任务。在这种情况下,您应该使用Queue或类似的方法来懒惰地创建它们。