简体   繁体   English

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

[英]Python synchronous code example faster than async

I was migrating a production system to async when I realized the synchronous version is 20x faster than the async version. 当我意识到同步版本比异步版本快20倍时,我正在将生产系统迁移到异步。 I was able to create a very simple example to demonstrate this in a repeatable way; 我能够创建一个非常简单的例子来以可重复的方式证明这一点;

Asynchronous Version 异步版本

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.")

This takes 19 seconds. 这需要19秒。 The code loops through 1M keys and builds a dictionary, data with the same key and value. 代码循环1M密钥并生成一个字典, data使用相同的键和值。

$ python3.7 async_test.py
Took 19.08 seconds.

Synchronous Version 同步版本

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.")

This takes 0.17 seconds! 这需要0.17秒! And does exactly the same thing as above. 并且与上面完全相同。

$ python3.7 test.py
Took 0.17 seconds.

Asynchronous Version with create_task 带有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.")

This version brings it down to 11 seconds. 这个版本将它降低到11秒。

$ python3.7 async_test2.py
Took 11.91 seconds.

Why does this happen? 为什么会这样?

In my production code I will have a blocking call in process_usage where I save the value of key to a redis database. 在我的生产代码中,我将在process_usage中进行阻塞调用,我将key的值保存到redis数据库中。

When comparing those benchmarks one should note that the asynchronous version is, well, asynchronous: asyncio spends a considerable effort to ensure that the coroutines you submit can run concurrently. 在比较这些基准测试时,应该注意异步版本是异步的:asyncio花费了大量精力来确保您提交的协同程序可以同时运行。 In your particular case they don't actually run concurrently because process_usage doesn't await anything, but the system doesn't actually that. 在您的特定情况下,它们并不实际并发运行,因为process_usage不会等待任何事情,但系统实际上并不是这样。 The synchronous version on the other hand makes no such provisions: it just runs everything sequentially, hitting the happy path of the interpreter. 另一方面,同步版本没有这样的规定:它只是按顺序运行所有内容,触及解释器的快乐路径。

A more reasonable comparison would be for the synchronous version to try to parallelize things in the way idiomatic for synchronous code: by using threads. 更合理的比较是同步版本尝试以同步代码的惯用方式并行化:通过使用线程。 Of course, you won't be able to create a separate thread for each process_usage because, unlike asyncio with its tasks, the OS won't allow you to create a million threads. 当然,您将无法为每个process_usage创建单独的线程,因为与其任务的asyncio不同,操作系统将不允许您创建一百万个线程。 But you can create a thread pool and feed it tasks: 但是您可以创建一个线程池并为其提供任务:

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

On my system this takes ~17s, whereas the asyncio version takes ~18s. 在我的系统上,这需要大约17秒,而asyncio版本需要大约18秒。 (The faster asyncio version takes ~13s.) (更快的asyncio版本需要大约13秒。)

If the speed gain of asyncio is so small, one could ask why bother with asyncio? 如果asyncio的速度增益很小,可以问为什么要打扰asyncio? The difference is that with asyncio, assuming idiomatic code and IO-bound coroutines, you have at your disposal a virtually unlimited number of tasks that in a very real sense execute concurrently. 不同之处在于,使用asyncio,假设惯用代码和IO绑定协程,您可以随意使用几乎无限数量的任务,这些任务在非常真实的意义上同时执行。 You can create tens of thousands of asynchronous connections at the same time, and asyncio will happily juggle them all at once, using a high-quality poller and a scalable coroutine scheduler. 您可以同时创建数以万计的异步连接,并且asyncio将使用高质量的轮询器和可扩展的协程调度程序一次性地将它们全部兼顾。 With a thread pool the number of tasks executed in parallel is always limited by the number of threads in the pool, typically in the hundreds at most. 对于线程池,并行执行的任务数总是受池中线程数的限制,通常最多为数百个。

Even toy examples have value, for learning if nothing else. 即使玩具的例子也有价值,如果没有其他的东西可以学 If you are using such microbenchmarks to make decisions, I suggest investing some more effort to give the examples more realism. 如果您使用此类微基准测试做出决策,我建议您投入更多精力来使示例更具真实性。 The coroutine in the asyncio example should contain at least one await , and the sync example should use threads to emulate the same amount of parallelism you obtain with async. asyncio示例中的协程应至少包含一个await ,同步示例应使用线程来模拟使用async获得的相同数量的并行性。 If you adjust both to match your actual use case, then the benchmark actually puts you in a position to make a (more) informed decision. 如果您调整两者以匹配您的实际用例,那么基准测试实际上使您能够做出更明智的决定。

Why does this happen? 为什么会这样?

TL;DR TL; DR

Because using asyncio itself doesn't speedup code. 因为使用asyncio本身并不能加速代码。 You need multiple gathered network I/O related operations to see the difference toward synchronous version. 您需要多个收集的网络I / O相关操作才能看到同步版本之间的差异。

Detailed 详细

asyncio is not a magic that allows you to speedup arbitrary code. asyncio不是一个允许你加速任意代码的魔法。 With or without asyncio your code is still being run by CPU with limit performance. 无论是否使用asyncio您的代码仍然由具有限制性能的CPU运行。

asyncio is a way to manage multiple execution flows (coroutines) in a nice, clear way. asyncio是一种以清晰,明确的方式管理多个执行流程(协同程序)的方法。 Multiple execution flows allow you to start next I/O-related operation (such as request to database) before waiting for other one to be completed. 多个执行流程允许您在等待其他操作完成之前启动下一个与I / O相关的操作(例如对数据库的请求)。 Please read this answer for more detailed explanation. 请阅读此答案以获得更详细的解释。

Please also read this answer for explanation when it makes sense to use asyncio . 当有意义使用asyncio时,请阅读此答案以获得解释。

Once you start to use asyncio right way overhead for using it should be much lower than benefits you get for parallelizing I/O operations. 一旦开始使用asyncio正确的方式开销使用它应该远远低于并行化I / O操作的好处。

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

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