¿Por qué asyncio no siempre usa ejecutores?

Tengo que enviar muchas solicitudes HTTP, una vez que todas han regresado, el progtwig puede continuar. Suena como una combinación perfecta para asyncio . Un poco ingenuamente, envolví mis llamadas a requests en una función async y las asyncio a asyncio . Esto no funciona.

Después de buscar en línea, encontré dos soluciones:

  • usa una biblioteca como aiohttp , que está hecha para trabajar con asyncio
  • envolver el código de locking en una llamada a run_in_executor

Para entender mejor esto, escribí un pequeño punto de referencia. El lado del servidor es un progtwig de matraz que espera 0.1 segundos antes de responder una solicitud.

 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() 

El cliente es mi punto de referencia.

 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 

Por lo tanto, una implementación intuitiva con asyncio no se ocupa del locking del código io. Pero si usa asyncio correctamente, es tan rápido como el marco especial aiohttp . Los documentos para coroutines y tareas realmente no mencionan esto. Solo si lees en el loop.run_in_executor () , dice:

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

Me sorprendió este comportamiento. El propósito de asyncio es acelerar el locking de llamadas io. ¿Por qué es necesario un contenedor adicional, run_in_executor , para hacer esto?

Todo el punto de venta de aiohttp parece ser el soporte para asyncio . Pero por lo que puedo ver, el módulo de requests funciona perfectamente, siempre y cuando lo envuelvas en un ejecutor. ¿Hay alguna razón para evitar envolver algo en un ejecutor?

Pero por lo que puedo ver, el módulo de solicitudes funciona perfectamente, siempre y cuando lo envuelvas en un ejecutor. ¿Hay alguna razón para evitar envolver algo en un ejecutor?

Ejecutar código en el ejecutor significa ejecutarlo en subprocesos del sistema operativo .

aiohttp y bibliotecas similares permiten ejecutar código no bloqueante sin subprocesos del sistema operativo, utilizando solo las rutinas.

Si no tiene mucho trabajo, la diferencia entre los subprocesos del sistema operativo y las rutinas no es significativa, especialmente si se compara con el cuello de botella: las operaciones de E / S. Pero una vez que tiene mucho trabajo, puede notar que los subprocesos del sistema operativo se comportan relativamente peor debido al cambio de contexto costoso.

Por ejemplo, cuando cambio su código a time.sleep(0.001) y range(100) , mi máquina muestra:

 asynchronous (executor) took 0.21461606299999997 seconds aiohttp took 0.12484742700000007 seconds 

Y esta diferencia solo boostá de acuerdo al número de solicitudes.

El propósito de asyncio es acelerar el locking de llamadas io.

No, el propósito de asyncio es proporcionar una forma conveniente de controlar el flujo de ejecución. asyncio permite elegir cómo funciona el flujo – basado en coroutines y subprocesos del sistema operativo (cuando usa ejecutor) o en coroutines puros (como hace aiohttp ).

El aiohttp de aiohttp es acelerar las cosas y hacer frente a la tarea como se muestra arriba 🙂