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

标签 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 中进行阻塞调用,我将 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 秒。 (更快的异步版本需要大约 13 秒。)

如果 asyncio 的速度增益如此之小,有人可能会问为什么要用 asyncio?不同之处在于,使用 asyncio,假定惯用代码和 IO 绑定(bind)协程,您可以随意处理几乎无限数量的任务,这些任务实际上是并发执行的。您可以同时创建数以万计的异步连接,asyncio 会使用高质量的轮询器和可扩展的协程调度程序愉快地同时处理所有这些连接。使用线程池,并行执行的任务数始终受池中线程数的限制,通常最多为数百个。

即使是玩具示例也很有值(value),如果没有别的学习的话。如果您正在使用这样的微基准来做出决定,我建议您投入更多的精力来让示例更加真实。 asyncio 示例中的协程应至少包含一个 await,而 sync 示例应使用线程来模拟与异步获得的相同数量的并行性。如果您调整两者以匹配您的实际用例,那么基准实际上可以让您做出(更)明智的决定。

关于比异步更快的 Python 同步代码示例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56026410/

相关文章:

python-3.x - Pandas 为多索引添加标题行

python - 通过 virtualenv 在 Mac 上安装旧版本的 Python

c# - 处理异步方法的取消

node.js - nodejs sendMessage 函数如果失败 X 次

python - AWS Lambda 读取作为源代码上传的 zip 文件的内容

python - statsmodels 逻辑回归类型问题

python - 如何根据特定的键值对比较两个 pandas 系列?

python - "or"似乎没有在 Python 中发挥应有的作用

python:无状态函数库设计

javascript - 向 Mongo 发送并行请求,并在所有请求完成后继续