Por Fran Palacios y Ariane Zanardi
Muchas veces, a lo largo de nuestra carrera nos toparemos con problemas de rendimiento y escalabilidad de las aplicaciones que desarrollamos. Gran parte de estos problemas surgen cuando por necesidades del proyecto, tenemos que realizar tareas que conllevan “mucho tiempo” como pueden ser la escritura/lectura de ficheros, peticiones HTTP, etc.
La programación asíncrona surge como una de las posibles soluciones a este problema, ya que haciendo uso de este estilo de programación, conseguimos que este tipo de tareas se ejecuten de manera independiente y, sin tener que esperar a que se completen, podemos seguir ejecutando el resto del código.
Python es un lenguaje “single-threaded” (de un sólo hilo), lo que significa que el intérprete de instrucciones solo puede ejecutar una cosa a la vez. Esto es debido a que utiliza un mecanismo de protección conocido como el Global Interpreter Lock (GIL), el cual limita la capacidad de Python para realizar el procesamiento de múltiples tareas en paralelo.
Para solventar esto, los lenguajes de un sólo hilo, como Python y JavaScript, entre otros, utilizan un mecanismo llamado “event loop”. El event loop es una estructura de control que maneja la ejecución de varias tareas de forma “simultánea”. Es decir, una tarea puede ser pausada y reanudada en cualquier momento, lo que permite que otras tareas puedan ser procesadas. Cuando una tarea se pausa, el event loop la suspende y comienza a procesar otra tarea. Cuando la tarea pausada está lista para continuar, el event loop la reanuda.
El event loop es el encargado de gestionar qué tarea pasa a ejecutarse en base a una prioridad. Estas tareas las almacena en una Tasks/Ready queue, que es una estructura de datos similar a un heap.
En Python, a partir de la versión 3.4, podemos utilizar la biblioteca asyncio para obtener programación asincrónica de manera nativa. Esta biblioteca nos ofrece la sintaxis async/await .
async le indica al interprete de Python que se trata de un función asíncrona, es decir, que dentro de esta función, vamos a poder realizar operaciones de forma asíncrona sin bloquear la ejecución principal de nuestro programa.
await se utiliza para indicar a nuestra función asíncrona, que vamos a esperar a que se resuelva una determinada operación, antes de seguir ejecutando el código restante.
Supongamos que tenemos el siguiente código:
import requests
from datetime import datetime
def synchronous_task():
start_time = datetime.now()
print("We are going to perform an synchronous operation")
for _ in range(10):
response = requests.get("https://dog.ceo/api/breeds/image/random")
print(response.json())
end_time = datetime.now()
print('Duration sync: {}'.format(end_time - start_time))
def main():
synchronous_task()
main()
Como podemos observar, este código se encarga de realizar 10 llamadas consecutivas a una api, tardando poco mas de 1 segundo.
We are going to perform an synchronous operation
0 -> {'message': 'https://images.dog.ceo/breeds/mastiff-english/4.jpg', 'status': 'success'}
1 -> {'message': 'https://images.dog.ceo/breeds/collie-border/n02106166_2111.jpg', 'status': 'success'}
2 -> {'message': 'https://images.dog.ceo/breeds/cotondetulear/100_2397.jpg', 'status': 'success'}
3 -> {'message': 'https://images.dog.ceo/breeds/terrier-sealyham/n02095889_5255.jpg', 'status': 'success'}
4 -> {'message': 'https://images.dog.ceo/breeds/mountain-bernese/n02107683_3694.jpg', 'status': 'success'}
5 -> {'message': 'https://images.dog.ceo/breeds/ovcharka-caucasian/IMG_20190826_112025.jpg', 'status': 'success'}
6 -> {'message': 'https://images.dog.ceo/breeds/sheepdog-english/n02105641_5341.jpg', 'status': 'success'}
7 -> {'message': 'https://images.dog.ceo/breeds/retriever-golden/n02099601_2358.jpg', 'status': 'success'}
8 -> {'message': 'https://images.dog.ceo/breeds/schnauzer-miniature/n02097047_1936.jpg', 'status': 'success'}
9 -> {'message': 'https://images.dog.ceo/breeds/terrier-silky/n02097658_329.jpg', 'status': 'success'}
Duration sync: 0:00:01.940478
Vamos a intentar mejorar este tiempo utilizando async/await. Para ello importamos asyncioy aiohttp (Cliente/Servidor HTTP asíncrono para asyncio y Python). Nuestro código quedaría de la siguiente manera:
import asyncio
import aiohttp
from datetime import datetime
async def asynchronous_task():
start_time = datetime.now()
print("We are going to perform an asynchronous operation")
async with aiohttp.ClientSession() as session:
for _ in range(10):
async with session.get("https://dog.ceo/api/breeds/image/random") as resp:
print(await resp.json())
end_time = datetime.now()
print('Duration async: {}'.format(end_time - start_time))
def main():
asyncio.run(asynchronous_task())
main()
Hemos realizado algunas mejoras en la definición de nuestra función al añadir el modificador async, lo que la convierte en una corutina, que son las que trabajan de forma efectiva con el event loop. Además, hemos incluido la palabra clave await
en el punto donde debemos esperar a que una tarea se realice.
En este caso, realizamos una petición HTTP, pero podría ser otra operación que pueda requerir algún tiempo. Es en ese momento, cuando el intérprete de Python se libera y el event loop asume el control para ejecutar otra tarea.
Si ejecutamos este programa obtenemos el siguiente resultado:
We are going to perform an asynchronous operation
0 -> {'message': 'https://images.dog.ceo/breeds/papillon/n02086910_2016.jpg', 'status': 'success'}
1 -> {'message': 'https://images.dog.ceo/breeds/samoyed/n02111889_3499.jpg', 'status': 'success'}
2 -> {'message': 'https://images.dog.ceo/breeds/australian-shepherd/forest.jpg', 'status': 'success'}
3 -> {'message': 'https://images.dog.ceo/breeds/terrier-russell/iguet4.jpg', 'status': 'success'}
4 -> {'message': 'https://images.dog.ceo/breeds/ridgeback-rhodesian/n02087394_10238.jpg', 'status': 'success'}
5 -> {'message': 'https://images.dog.ceo/breeds/pyrenees/n02111500_2151.jpg', 'status': 'success'}
6 -> {'message': 'https://images.dog.ceo/breeds/spaniel-cocker/n02102318_10178.jpg', 'status': 'success'}
7 -> {'message': 'https://images.dog.ceo/breeds/keeshond/n02112350_7115.jpg', 'status': 'success'}
8 -> {'message': 'https://images.dog.ceo/breeds/setter-gordon/n02101006_1814.jpg', 'status': 'success'}
9 -> {'message': 'https://images.dog.ceo/breeds/terrier-fox/n02095314_481.jpg', 'status': 'success'}
Duration async: 0:00:00.784688
Como podemos observar, hemos mejorado el rendimiento del programa. ¿Qué pasaría si aumentamos el número de peticiones a 100? Pues obtendríamos lo siguiente:
We are going to perform an synchronous operation
0 -> {'message': 'https://images.dog.ceo/breeds/terrier-dandie/n02096437_2508.jpg', 'status': 'success'}
1 -> {'message': 'https://images.dog.ceo/breeds/hound-afghan/n02088094_2062.jpg', 'status': 'success'}
2 -> {'message': 'https://images.dog.ceo/breeds/terrier-patterdale/Patterdale.jpg', 'status': 'success'}
3 -> {'message': 'https://images.dog.ceo/breeds/terrier-bedlington/n02093647_3114.jpg', 'status': 'success'}
4 -> {'message': 'https://images.dog.ceo/breeds/dingo/n02115641_13450.jpg', 'status': 'success'}
5 -> {'message': 'https://images.dog.ceo/breeds/keeshond/n02112350_212.jpg', 'status': 'success'}
.......
96 -> {'message': 'https://images.dog.ceo/breeds/clumber/n02101556_7295.jpg', 'status': 'success'}
97 -> {'message': 'https://images.dog.ceo/breeds/sharpei/noel.jpg', 'status': 'success'}
98 -> {'message': 'https://images.dog.ceo/breeds/hound-plott/hhh_plott002.jpg', 'status': 'success'}
99 -> {'message': 'https://images.dog.ceo/breeds/terrier-fox/n02095314_1754.jpg', 'status': 'success'}
Duration sync: 0:00:19.994999
We are going to perform an asynchronous operation
0 -> {'message': 'https://images.dog.ceo/breeds/germanshepherd/n02106662_11620.jpg', 'status': 'success'}
1 -> {'message': 'https://images.dog.ceo/breeds/ridgeback-rhodesian/n02087394_11442.jpg', 'status': 'success'}
2 -> {'message': 'https://images.dog.ceo/breeds/terrier-yorkshire/img_2114.jpg', 'status': 'success'}
3 -> {'message': 'https://images.dog.ceo/breeds/hound-plott/hhh-23456.jpg', 'status': 'success'}
4 -> {'message': 'https://images.dog.ceo/breeds/mastiff-bull/n02108422_2353.jpg', 'status': 'success'}
5 -> {'message': 'https://images.dog.ceo/breeds/waterdog-spanish/20180723_185559.jpg', 'status': 'success'
.......
96 -> {'message': 'https://images.dog.ceo/breeds/mix/Annabelle2.jpg', 'status': 'success'}
97 -> {'message': 'https://images.dog.ceo/breeds/maltese/n02085936_352.jpg', 'status': 'success'}
98 -> {'message': 'https://images.dog.ceo/breeds/setter-english/n02100735_3348.jpg', 'status': 'success'}
99 -> {'message': 'https://images.dog.ceo/breeds/spaniel-irish/n02102973_2763.jpg', 'status': 'success'}
Duration async: 0:00:06.937758
Aquí ya podemos apreciar mejor la diferencia en el rendimiento, siendo que las peticiones HTTP hechas de forma asíncrona tardan casi 7 segundos, frente a los 20 segundos de las peticiones síncronas.
Cabe destacar, que esta es una solución utilizando las herramientas que nos proporciona la librería estándar de Python. Existen otras librerías que nos permiten manejar este tipo de código, e incluso, podemos superar ciertas limitaciones de la librería estándar.
Podemos concluir que el código asíncrono ofrece varias ventajas, como una mayor eficiencia, al permitir la realización de múltiples tareas “simultáneamente” sin bloquear el hilo principal, aprovechando la CPU y reduciendo los tiempos de espera. Además, podemos mejorar la escalabilidad del sistema, ya que puede manejar más solicitudes al mismo tiempo. Sin embargo, es importante tener en cuenta que la escritura y comprensión del código asíncrono, puede ser más compleja que la del código síncrono, lo que a su vez puede dificultar la depuración y la corrección de errores.
https://docs.python.org/3/library/asyncio.html
https://docs.python.org/3/library/asyncio-task.html
https://realpython.com/python-gil/
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista