繁体   English   中英

Python同步代码示例比异步更快

[英]Python synchronous code example faster than async

当我意识到同步版本比异步版本快20倍时,我正在将生产系统迁移到异步。 我能够创建一个非常简单的例子来以可重复的方式证明这一点;

异步版本

import asyncio, time

data = {}

async def process_usage(key):
    data[key] = key

async def main():
    await asyncio.gather(*(process_usage(key) for key in range(0,1000000)))

s = time.perf_counter()
results = asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"Took {elapsed:0.2f} seconds.")

这需要19秒。 代码循环1M密钥并生成一个字典, data使用相同的键和值。

$ python3.7 async_test.py
Took 19.08 seconds.

同步版本

import time

data = {}

def process_usage(key):
    data[key] = key

def main():
    for key in range(0,1000000):
        process_usage(key)

s = time.perf_counter()
results = main()
elapsed = time.perf_counter() - s
print(f"Took {elapsed:0.2f} seconds.")

这需要0.17秒! 并且与上面完全相同。

$ python3.7 test.py
Took 0.17 seconds.

带有create_task异步版本

import asyncio, time

data = {}

async def process_usage(key):
    data[key] = key

async def main():
    for key in range(0,1000000):
        asyncio.create_task(process_usage(key))

s = time.perf_counter()
results = asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"Took {elapsed:0.2f} seconds.")

这个版本将它降低到11秒。

$ python3.7 async_test2.py
Took 11.91 seconds.

为什么会这样?

在我的生产代码中,我将在process_usage中进行阻塞调用,我将key的值保存到redis数据库中。

在比较这些基准测试时,应该注意异步版本是异步的:asyncio花费了大量精力来确保您提交的协同程序可以同时运行。 在您的特定情况下,它们并不实际并发运行,因为process_usage不会等待任何事情,但系统实际上并不是这样。 另一方面,同步版本没有这样的规定:它只是按顺序运行所有内容,触及解释器的快乐路径。

更合理的比较是同步版本尝试以同步代码的惯用方式并行化:通过使用线程。 当然,您将无法为每个process_usage创建单独的线程,因为与其任务的asyncio不同,操作系统将不允许您创建一百万个线程。 但是您可以创建一个线程池并为其提供任务:

def main():
    with concurrent.futures.ThreadPoolExecutor() as executor:
        for key in range(0,1000000):
            executor.submit(process_usage, key)
        # at the end of "with" the executor automatically
        # waits for all futures to finish

在我的系统上,这需要大约17秒,而asyncio版本需要大约18秒。 (更快的asyncio版本需要大约13秒。)

如果asyncio的速度增益很小,可以问为什么要打扰asyncio? 不同之处在于,使用asyncio,假设惯用代码和IO绑定协程,您可以随意使用几乎无限数量的任务,这些任务在非常真实的意义上同时执行。 您可以同时创建数以万计的异步连接,并且asyncio将使用高质量的轮询器和可扩展的协程调度程序一次性地将它们全部兼顾。 对于线程池,并行执行的任务数总是受池中线程数的限制,通常最多为数百个。

即使玩具的例子也有价值,如果没有其他的东西可以学 如果您使用此类微基准测试做出决策,我建议您投入更多精力来使示例更具真实性。 asyncio示例中的协程应至少包含一个await ,同步示例应使用线程来模拟使用async获得的相同数量的并行性。 如果您调整两者以匹配您的实际用例,那么基准测试实际上使您能够做出更明智的决定。

为什么会这样?

TL; DR

因为使用asyncio本身并不能加速代码。 您需要多个收集的网络I / O相关操作才能看到同步版本之间的差异。

详细

asyncio不是一个允许你加速任意代码的魔法。 无论是否使用asyncio您的代码仍然由具有限制性能的CPU运行。

asyncio是一种以清晰,明确的方式管理多个执行流程(协同程序)的方法。 多个执行流程允许您在等待其他操作完成之前启动下一个与I / O相关的操作(例如对数据库的请求)。 请阅读此答案以获得更详细的解释。

当有意义使用asyncio时,请阅读此答案以获得解释。

一旦开始使用asyncio正确的方式开销使用它应该远远低于并行化I / O操作的好处。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM