[PYTHON] Comparativa realización peticiones HTTP asíncronas

in #python8 years ago (edited)

opengraph-icon-200x200.png

English Version

Una de las novedades mas destacadas que ha traído la versión 3.X ha sido la programación asíncrona. A partir de la versión 3.4 se incorporó el módulo asyncio, con el que a través del decorador @asyncio.coroutine permitió definir corrutinas. Estas son una generalización de las subrutinas que permite detener y continuar la ejecución en un punto.

En versiones posteriores se añadió sintaxis mas cómoda con la que implementar las corrutinas: await y async.

Sin entrar en detalles, el flujo consiste en ejecutar un event loop, el motor central que se encarga de ir controlando que corrutinas se ejecutan en cada momento a partir de los eventos del sistema (recepción de un socket, lectura de base de datos, etc.). Donde se lanzan una (o varias) tareas asíncronas, que a su vez, estas pueden lanzar otras y la ejecución va pasando de unas a otras.

Por ejemplo, un "hola mundo", que pinta un mensaje y espera un segundo sería:

import asyncio

async def hello_world():
    print("Hello World!")
    await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(hello_world())
loop.close()

Como asyncio ya es bastante maduro y utilizado, hay numerosas bibliotecas para facilitar el uso de corrutinas y realizar las tareas mas comunes.

Ejemplos de estas son:

Ejemplos de mas bibliotecas se pueden encontrar en las aio-libs.

Una de las tareas mas útiles que se puede realizar asíncronamente es la de hacer varias peticiones HTTP. Lo normal es que para realizar una petición se tarden varios milisegundos, por lo que sería útil que mientras se esta haciendo una, ir lanzando las demás.
Y finalmente recoger los resultados.

Para poder probarlas, he creado un pequeño benchmark, que realiza varias peticiones. En los tres casos, excepto para aiohttp, he utilizado requests. Existe la biblioteca asks, que se integra con curio y trio, para usarse como reemplazo de requests, pero me daba errores en la versión de Python que estaba utilizando y finalmente he optado por no emplearla.

Como caso base, he partido de los tiempos de un ejemplo síncrono.

def run(urls):
    for url in urls:
        r = requests.get(url)

Después he implementado una versión asíncrona nativa. Es decir, puramente con asyncio.

async def main(loop, urls):
    futures = []

    for url in urls:
        future = loop.run_in_executor(None, requests.get, url)
        futures.append(future)

    for future in futures:
        r = await future  
    

def run(urls):
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop, urls))

El ejemplo de aiohttp es bastante directo, extraído de la documentación:

async def fetch(session, url):
    with async_timeout.timeout(50):
        async with session.get(url) as response:
            return await response.text(encoding="iso8859-1")

async def main(urls):
    async with aiohttp.ClientSession() as session:
        for url in urls:
            await fetch(session, url)

def run(urls):
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(urls))

Para curio he utilizado la función spawn que permite lanzar tareas. Luego hay que realizar un join para esperar a que esta termine.

async def fetch(url):
    requests.get(url)

async def main(url_list):
    tasks = []
    for url in url_list:
        task = await curio.spawn(fetch(url))
        tasks.append(task)

    for task in tasks:
        await task.join()
    

def run(urls):
    curio.run(main(urls))

Finalmente trio permite crear un pool de corutinas mediante un contexto, en el que para finalizar, espera a que las tareas terminen.

async def fetch(url):
    requests.get(url)

async def main(urls):
    async with trio.open_nursery() as nursery:
        for url in urls:
            nursery.start_soon(fetch, url)

def run(urls):
    trio.run(main, urls)

Para realizar las pruebas he consultado a 50 urls, calculando la media en tandas de 10 iteraciones.

ImplementaciónTiempos
Synchronous26.95 s.
Native Async2.87 s.
AioHttp Client21.14 s.
Curio22.45 s.
Trio23.28 s.

Como se puede observar, las versiones asíncronas son mas rápidas que la síncrona. Siendo la mejor la nativa. Bien es cierto que la diferencia es tan grande, que probablemente haya algún error en la implementación en alguno de los ejemplos.

El código fuente completo y el lanzador de los test, se puede consultar en Github

Coin Marketplace

STEEM 0.13
TRX 0.34
JST 0.035
BTC 111231.91
ETH 4327.40
SBD 0.83