【问题标题】:Why doesn't asyncio always use executors?为什么 asyncio 不总是使用执行器?
【发布时间】:2019-04-15 02:05:17
【问题描述】:

我必须发送很多 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()

客户是我的基准

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.

我对这种行为感到惊讶。 asyncio 的目的是加速阻塞 io 调用。为什么需要额外的包装器run_in_executor 来执行此操作?

aiohttp 的全部卖点似乎是对asyncio 的支持。但据我所知,requests 模块可以完美运行——只要你将它包装在一个执行器中。是否有理由避免在 executor 中包装一些东西?

【问题讨论】:

  • 一般来说,ayncio 的目的不是为了加快速度,而是为了减少延迟。您的两种方法都可以做到这一点,而执行程序可能需要更多资源。
  • 执行器是基于线程的。 asyncio 使用非阻塞套接字,因此它可以用一个线程请求多个,但 requests 不是

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


【解决方案1】:

但据我所知,请求模块运行良好——只要 当您将其包装在执行程序中时。是否有理由避免包装 executor 里的东西?

在执行器中运行代码意味着在OS threads中运行。

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

如果您没有太多工作,操作系统线程和协程之间的差异并不显着,尤其是与瓶颈 - 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 允许您选择流程的工作方式 - 基于协程和操作系统线程(当您使用执行程序时)或纯协程(如 aiohttp 那样)。

aiohttp 的目的是加快速度,它可以应付如上所示的任务:)

【讨论】:

  • Asyncio 协程并不是真正的绿色线程,因为绿色线程是可堆叠的。携带完整的堆栈允许它们在任意位置切换并避免function color 问题,但代价是每个绿色线程都比协程/fiber 更重。 Python 实现绿色线程的一个例子是greenlet 模块和基于它的gevent 事件循环。
  • @user4815162342 感谢您的澄清!我改变了答案。
  • @MikhailGerasimov,感谢您对 aiohttps 性能的详细阐述,我 +1 :) 我仍然有一些概念问题,目前正在更新我的问题
  • 我已经更新了我的问题。我不明白 asyncio 和 aiohttp 之间的交集。 Asyncio 有没有操作系统线程的非阻塞协程?这听起来像是一个巨大的功能。这是 asyncio 的一部分吗?如果是,为什么不是默认值。如果不是,aiohttp 是如何基于 asyncio 的(async/await 是一种语言特性,不直接属于 asyncio)?
  • @lhk 是的,asyncio 有无操作系统线程的非阻塞协程,它一个巨大的特性。 Aiohttp 基于 asyncio,因为它依赖于构建在原始 async/await 之上的 asyncio 抽象。请参阅this question 的答案,尤其是this one,以深入了解该主题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-21
  • 1970-01-01
  • 2018-05-08
相关资源
最近更新 更多