【问题标题】:What is the overhead of an asyncio task? [closed]异步任务的开销是多少? [关闭]
【发布时间】:2019-09-09 17:07:13
【问题描述】:

任何 asyncio 任务在内存和速度方面的开销是多少?在不需要同时运行的情况下,是否值得尽量减少任务数量?

【问题讨论】:

  • 这是一个相当广泛的问题……问题是,它对您来说足够高效吗?串行执行相同的任务可能意味着整个操作需要更长的时间;而异步执行它们可能会更快地完成它们。当然有资源与时间的权衡。你需要弄清楚哪些资源对你来说更宝贵,哪些是你能负担得起的,花多少钱。您最好通过对实际代码进行基准测试来做到这一点。
  • 与什么有关?线程?正常功能?流程?全部?

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


【解决方案1】:

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

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

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

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

没有理由直接写 (2),但是当使用自动 "futurize" 他们收到的可等待对象的 API 时,很容易创建不必要的任务,例如 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

在我运行 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 到事件循环。相反,它会立即开始执行协程,就好像它是一个普通函数一样。

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

【讨论】:

  • 感谢您提供所有详细信息...不过有一个问题,`coros = [noop() for _ in range(1000000)]` 是否真的安排所有 noops 运行?跨度>
  • @MichalCharemza 不是,自动调度是上层Task的属性,不是下层协程对象的。在内存基准测试中,创建一百万个它们只是为了使内存使用情况变得明显,而不是假装实际等待它们的运行时语义是相同的。
  • 暂停似乎是这里最重要的部分:如果我将代码更改为async def noop(): asyncio.sleep(0),我会得到10 sec. vs 30 sec.。我不确定我是否购买了关于coroutine is simple enough 的论点:如果它不会暂停,就没有必要创建协程,尤其是数以百万计的协程。不过,感谢您的研究!
  • @MikhailGerasimov 如果协程不会暂停,则无需创建协程我不考虑永远不会暂停的协程,但是一个可能不会暂停通常。答案提到了stream.read()作为一个例子,它的工作原理与此完全一样,但还有其他例子,例如queue.getqueue.put,许多异步上下文管理器上的__aenter__方法,非竞争中的同步方法案例等。有许多低级协程在等待时不会每次都挂起。
【解决方案2】:

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

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

通常 (*) 您不必考虑任务数量,例如,您通常不需要考虑 Python 脚本中的函数调用数量。

当然,您应该始终考虑异步程序的一般工作方式。如果它要同时发出大量 I/O 请求或产生大量同时线程/进程,您应该使用 Semaphore 以避免同时获取太多资源。


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

【讨论】:

    猜你喜欢
    • 2018-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多