Почему Asyncio не всегда использует исполнителей? - PullRequest
0 голосов
/ 12 ноября 2018

Мне нужно отправить много 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 Ответ

0 голосов
/ 12 ноября 2018

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

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

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

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

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

asynchronous (executor) took 0.21461606299999997 seconds
aiohttp took 0.12484742700000007 seconds

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

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

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

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

...