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

时间:2019-05-07 15:56:53

标签: python python-3.x async-await python-asyncio

当我意识到同步版本比异步版本快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中进行阻塞调用,将密钥值保存到Redis数据库中。

2 个答案:

答案 0 :(得分:6)

在比较这些基准测试时,应该注意异步版本是异步的: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秒,而异步版本大约需要18秒。 (更快的异步版本大约需要13秒。)

如果asyncio的速度增益太小,人们可能会问为什么要烦恼asyncio?区别在于,使用异步时,假设惯用代码和IO绑定协程,您可以随意使用几乎无限数量的任务,这些任务实际上可以并发执行。您可以同时创建数万个异步连接,并且asyncio将使用高质量的轮询器和可伸缩的协程调度程序一次愉快地处理所有异步连接。对于线程池,并行执行的任务数始终受池中线程数的限制,通常最多为数百个。

甚至玩具实例也很有价值,对于学习无非。如果您使用这样的微基准进行决策,我建议您花更多的精力使示例更加真实。异步示例中的协程应至少包含一个await,同步示例应使用线程来模拟与异步获得的并行量。如果您都调整了两者以匹配您的实际用例,那么基准实际上会使您处于做出(更多)知情决定的位置。

答案 1 :(得分:1)

  

为什么会这样?

TL; DR

因为使用asyncio本身不会加快代码的速度。您需要进行多个与网络I / O相关的收集操作,才能看到同步版本的不同之处。

详细

asyncio并不是让您加速任意代码的魔力。不论是否使用asyncio,您的代码仍在CPU的限制下运行。

asyncio是一种以一种简洁明了的方式管理多个执行流(协程)的方法。多个执行流使您可以在等待其他I / O相关操作完成之前开始下一个与I / O相关的操作(例如对数据库的请求)。请阅读this answer了解更多详细说明。

在合理使用asyncio时,也请阅读this answer进行解释。

一旦开始正确使用asyncio,使用它的开销应该比并行I / O操作所获得的收益要低得多。