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

lhk*_*lhk 3 python coroutine python-requests python-asyncio aiohttp

我必须发送大量HTTP请求,一旦所有HTTP请求都返回,程序就可以继续.听起来像是一场完美的比赛asyncio.有点天真,我把我的电话包裹requests在一个async函数中,然后把它们给了asyncio.这不起作用.

在线搜索后,我找到了两个解决方案:

  • 使用像aiohttp这样的库,可以使用它asyncio
  • 在调用中包装阻塞代码 run_in_executor

为了更好地理解这一点,我写了一个小基准.服务器端是一个烧瓶程序,在回答请求之前等待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()
Run Code Online (Sandbox Code Playgroud)

客户是我的基准

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
Run Code Online (Sandbox Code Playgroud)

因此,直观的实现asyncio并不涉及阻止io代码.但是如果你asyncio正确使用它,它就像特殊aiohttp框架一样快.协程和任务的文档并没有真正提到这一点.只有你读完loop.run_in_executor(),它才会说:

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

我对这种行为感到惊讶.asyncio的目的是加速阻止io调用.为什么需要一个额外的包装器run_in_executor呢?

整个卖点aiohttp似乎是支持asyncio.但据我所知,该requests模块运行完美 - 只要将其包装在执行程序中即可.是否有理由避免在执行人中包装某些内容?

Mik*_*mov 7

但据我所知,请求模块完美运行 - 只要将其包装在执行程序中.是否有理由避免在执行人中包装某些内容?

在执行程序中运行代码意味着在OS线程中运行它.

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

如果你没有太多工作,OS线程和协同程序之间的区别并不重要,特别是与瓶颈 - I/O操作相比.但是一旦你有大量的工作,你就会注意到由于昂贵的上下文切换,操作系统线程的性能相对较差.

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

asynchronous (executor) took 0.21461606299999997 seconds
aiohttp took 0.12484742700000007 seconds
Run Code Online (Sandbox Code Playgroud)

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

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

不,目的asyncio是提供控制执行流程的便捷方式.asyncio允许您选择流的工作方式 - 基于协同程序和操作系统线程(当您使用执行程序时)或纯协同程序(如同aiohttp).

它的aiohttp目的是加速事情,它应对如上所示的任务:)

  • Asyncio协同程序并不是真正的绿色线程,因为绿色线程是堆叠的.携带完整的堆栈允许他们在任意位置切换并避免[功能颜色](http://journal.stuffwithstuff.com/2015/02/01/what-c​​olor-is-your-function/)问题,但是每个绿色线程的成本比协程/ [光纤]重得多(https://en.wikipedia.org/wiki/Fiber_(computer_science)).绿色线程的Python实现的一个例子是[greenlet](https://pypi.org/project/greenlet/)模块和基于它的[gevent](http://www.gevent.org/)事件循环. (2认同)