Почему Asyncio не всегда использует исполнителей?

Я должен отправить много 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 фреймворк. Документы для сопрограмм и задач на самом деле не упоминают об этом. Только если вы читаете в loop.run_in_executor (), он говорит:

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

Я был удивлен таким поведением. Цель asyncio - ускорить блокировку вызовов. Почему дополнительная обертка, run_in_executorнеобходимо сделать это?

Весь смысл продажи aiohttp кажется, поддержка asyncio, Но, насколько я понимаю, requests Модуль работает отлично - до тех пор, пока вы его заверните в исполнителя. Есть ли причина избегать того, чтобы что-то оборачивать в исполнителя?

1 ответ

Решение

Но, насколько я вижу, модуль запросов работает отлично - до тех пор, пока вы заключаете его в исполнителя. Есть ли причина избегать того, чтобы что-то оборачивать в исполнителя?

Запуск кода в executor означает запуск его в потоках ОС.

aiohttp и подобные библиотеки позволяют запускать неблокирующий код без потоков ОС, используя только сопрограммы.

Если у вас мало работы, разница между потоками ОС и сопрограммами незначительна, особенно по сравнению с операциями ввода-вывода с узким местом. Но когда у вас много работы, вы можете заметить, что потоки ОС работают относительно хуже из-за дорогостоящего переключения контекста.

Например, когда я изменяю ваш код на time.sleep(0.001) а также range(100)Моя машина показывает:

asynchronous (executor) took 0.21461606299999997 seconds
aiohttp took 0.12484742700000007 seconds

И эта разница будет только увеличиваться в зависимости от количества запросов.

Цель asyncio - ускорить блокировку вызовов.

Нет, цель asyncio это предоставить удобный способ контролировать выполнение потока. asyncio позволяет выбрать, как работает поток - на основе сопрограмм и потоков ОС (когда вы используете executor) или на чистых сопрограммах (например, aiohttp делает).

Это aiohttpЦель ускорить процесс, и он справляется с задачей, как показано выше:)

Другие вопросы по тегам