为什么asyncio不总是使用执行程序?

时间:2018-11-12 10:57:39

标签: python python-requests python-asyncio coroutine aiohttp

我必须发送很多HTTP请求,一旦所有HTTP请求返回,程序就可以继续。听起来asyncio非常合适。天真地,我将对requests的调用包装在async函数中,然后将其交给asyncio。这行不通。

在线搜索后,我发现了两种解决方案:

  • 使用类似aiohttp的库,该库可与asyncio一起使用
  • 在对run_in_executor的调用中包装阻塞代码

为了更好地理解这一点,我编写了一个小型基准。服务器端是Flask程序,在等待请求之前等待0.1秒。

from flask import Flask
import time

app = Flask(__name__)


@app.route('/')
def hello_world():
    time.sleep(0.1) // heavy calculations here :)
    return 'Hello World!'


if __name__ == '__main__':
    app.run()

客户是我的基准

import requests
from time import perf_counter, sleep

# this is the baseline, sequential calls to requests.get
start = perf_counter()
for i in range(10):
    r = requests.get("http://127.0.0.1:5000/")
stop = perf_counter()
print(f"synchronous took {stop-start} seconds") # 1.062 secs

# now the naive asyncio version
import asyncio
loop = asyncio.get_event_loop()

async def get_response():
    r = requests.get("http://127.0.0.1:5000/")

start = perf_counter()
loop.run_until_complete(asyncio.gather(*[get_response() for i in range(10)]))
stop = perf_counter()
print(f"asynchronous took {stop-start} seconds") # 1.049 secs

# the fast asyncio version
start = perf_counter()
loop.run_until_complete(asyncio.gather(
    *[loop.run_in_executor(None, requests.get, 'http://127.0.0.1:5000/') for i in range(10)]))
stop = perf_counter()
print(f"asynchronous (executor) took {stop-start} seconds") # 0.122 secs

#finally, aiohttp
import aiohttp

async def get_response(session):
    async with session.get("http://127.0.0.1:5000/") as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        await get_response(session)

start = perf_counter()
loop.run_until_complete(asyncio.gather(*[main() for i in range(10)]))
stop = perf_counter()
print(f"aiohttp took {stop-start} seconds") # 0.121 secs

因此,使用asyncio进行的直观实现不会处理阻塞io代码。但是,如果正确使用asyncio,它与特殊aiohttp框架一样快。 coroutines and tasks的文档并未真正提及这一点。仅当您阅读loop.run_in_executor()时,它才会说:

# File operations (such as logging) can block the
# event loop: run them in a thread pool.

我对这种行为感到惊讶。异步的目的是加快阻塞io调用。为什么需要额外的包装run_in_executor

aiohttp的整个卖点似乎是对asyncio的支持。但据我所知,requests模块可以完美运行-只要将其包装在执行程序中即可。是否有理由避免将某些东西包装在执行器中?

1 个答案:

答案 0 :(得分:6)

  

但是据我所知,请求模块可以完美地工作-只要   当您将其包装在执行程序中时。是否有避免包裹的理由   遗嘱执行人有事吗?

在执行程序中运行代码意味着要在OS threads中运行它。

aiohttp和类似的库允许仅使用协程在没有OS线程的情况下运行非阻塞代码。

如果您的工作量不大,则OS线程和协程之间的差异并不明显,尤其是与瓶颈-I / O操作相比。但是一旦您做了很多工作,您就会注意到由于昂贵的context switching,操作系统线程的性能相对较差。

例如,当我将您的代码更改为time.sleep(0.001)range(100)时,我的机器将显示:

asynchronous (executor) took 0.21461606299999997 seconds
aiohttp took 0.12484742700000007 seconds

这种差异只会根据请求数而增加。

  

asyncio的目的是加速阻止io调用。

不是,asyncio的目的是提供方便的方法来控制执行流程。 asyncio允许您基于协程和OS线程(使用执行程序时)或基于纯协程(如aiohttp来选择流程的工作方式。)

aiohttp的目的是加快处理速度,它可以应付上述任务:)