Comience con async en Python

La programación asincrónica, o async para abreviar, es una característica de muchos lenguajes modernos que permite a un programa hacer malabarismos con múltiples operaciones sin esperar o colgarse de ninguno de ellos. Es una forma inteligente de manejar de manera eficiente tareas como la E / S de red o archivos, donde la mayor parte del tiempo del programa se gasta esperando a que finalice una tarea.

Considere una aplicación de raspado web que abre 100 conexiones de red. Puede abrir una conexión, esperar los resultados, luego abrir la siguiente y esperar los resultados, y así sucesivamente. La mayor parte del tiempo que se ejecuta el programa se dedica a esperar una respuesta de la red, no a realizar el trabajo real.

Async le brinda un método más eficiente: abra las 100 conexiones a la vez, luego cambie entre cada conexión activa a medida que arrojan resultados. Si una conexión no devuelve resultados, cambie a la siguiente, y así sucesivamente, hasta que todas las conexiones hayan devuelto sus datos.

La sintaxis asíncrona es ahora una característica estándar en Python, pero los Pythonistas de toda la vida que están acostumbrados a hacer una cosa a la vez pueden tener problemas para entenderlo. En este artículo exploraremos cómo funciona la programación asincrónica en Python y cómo ponerla en práctica.

Tenga en cuenta que si desea usar async en Python, es mejor usar Python 3.7 o Python 3.8 (la última versión al momento de escribir este artículo). Usaremos la sintaxis asíncrona de Python y las funciones auxiliares definidas en esas versiones del lenguaje.

Cuándo usar la programación asincrónica

En general, los mejores momentos para usar async son cuando intenta hacer un trabajo que tiene las siguientes características:

  • El trabajo tarda mucho en completarse.
  • El retraso implica esperar las operaciones de E / S (disco o red), no el cálculo.
  • El trabajo involucra muchas operaciones de E / S que ocurren a la vez, o una o más operaciones de E / S que ocurren cuando también está tratando de realizar otras tareas.

Async le permite configurar múltiples tareas en paralelo e iterarlas de manera eficiente, sin bloquear el resto de su aplicación.

Algunos ejemplos de tareas que funcionan bien con async:

  • Web scraping, como se describe arriba.
  • Servicios de red (por ejemplo, un servidor web o un marco).
  • Programas que coordinan resultados de múltiples fuentes que tardan mucho en devolver valores (por ejemplo, consultas simultáneas de bases de datos).

Es importante tener en cuenta que la programación asincrónica es diferente del multiproceso o multiprocesamiento. Todas las operaciones asíncronas se ejecutan en el mismo subproceso, pero ceden entre sí según sea necesario, lo que hace que el asíncrono sea más eficiente que el subproceso o el multiprocesamiento para muchos tipos de tareas. (Más sobre esto a continuación).

Python asyncawaityasyncio

Python agregó recientemente dos palabras clave asyncy await, para crear operaciones asíncronas. Considere este script:

def get_server_status (server_addr) # Una operación potencialmente de larga duración ... return server_status def server_ops () results = [] results.append (get_server_status ('addr1.server') results.append (get_server_status ('addr2.server') return resultados 

Una versión asíncrona del mismo script, no funcional, solo lo suficiente para darnos una idea de cómo funciona la sintaxis, podría verse así.

async def get_server_status (server_addr) # Una operación potencialmente de larga duración ... return server_status async def server_ops () results = [] results.append (await get_server_status ('addr1.server') results.append (await get_server_status ('addr2. servidor ') devuelve resultados 

Las funciones con el prefijo de la asyncpalabra clave se convierten en funciones asincrónicas, también conocidas como corrutinas . Las corrutinas se comportan de manera diferente a las funciones regulares:

  • Las corrutinas pueden usar otra palabra clave, lo awaitque permite que una corrutina espere los resultados de otra corrutina sin bloquear. Hasta que los resultados regresen de la awaitcorrutina ed, Python cambia libremente entre otras corrutinas en ejecución.
  • Las corrutinas solo se pueden llamar desde otras asyncfunciones. Si ejecuta server_ops()o get_server_status()como está desde el cuerpo del script, no obtendrá sus resultados; obtendrá un objeto de rutina de Python, que no se puede usar directamente.

Entonces, si no podemos llamar a asyncfunciones desde funciones no asincrónicas y no podemos ejecutar asyncfunciones directamente, ¿cómo las usamos? Respuesta: Usando la asynciobiblioteca, que une asyncal resto de Python.

Python asyncawaity asyncioejemplo

Aquí hay un ejemplo (nuevamente, no funcional pero ilustrativo) de cómo se podría escribir una aplicación de web scraping usando asyncy asyncio. Este script toma una lista de URL y usa varias instancias de una asyncfunción de una biblioteca externa ( read_from_site_async()) para descargarlas y agregar los resultados.

importar asyncio desde web_scraping_library importar read_from_site_async async def main (url_list): return await asyncio.gather (* [read_from_site_async (_) for _ in url_list]) urls = ['//site1.com','//othersite.com', '//newsite.com'] resultados = asyncio.run (main (urls)) print (resultados) 

En el ejemplo anterior, usamos dos asynciofunciones comunes :

  • asyncio.run()se utiliza para iniciar una asyncfunción desde la parte no asíncrona de nuestro código y, por lo tanto, iniciar todas las actividades asíncronas del programa. (Así es como corremos main()).
  • asyncio.gather()toma una o más funciones decoradas de forma asíncrona (en este caso, varias instancias de read_from_site_async()de nuestra hipotética biblioteca de raspado web), las ejecuta todas y espera a que lleguen todos los resultados.

La idea aquí es que comenzamos la operación de lectura para todos los sitios a la vez, luego recopilamos los resultados a medida que llegan (por lo tanto asyncio.gather()). No esperamos a que se complete ninguna operación antes de pasar a la siguiente.

Componentes de las aplicaciones asíncronas de Python

Ya hemos mencionado cómo las aplicaciones asíncronas de Python usan corrutinas como su ingrediente principal, basándose en la asynciobiblioteca para ejecutarlas. Algunos otros elementos también son clave para las aplicaciones asincrónicas en Python:

Bucles de eventos

La asynciobiblioteca crea y administra bucles de eventos , los mecanismos que ejecutan corrutinas hasta que se completan. Solo se debe ejecutar un ciclo de eventos a la vez en un proceso de Python, aunque solo sea para facilitar que el programador realice un seguimiento de lo que contiene.

Tareas

Cuando envía una corrutina a un bucle de eventos para su procesamiento, puede recuperar un Taskobjeto, que proporciona una forma de controlar el comportamiento de la corrutina desde fuera del bucle de eventos. Si necesita cancelar la tarea en ejecución, por ejemplo, puede hacerlo llamando al .cancel()método de la tarea .

Aquí hay una versión ligeramente diferente del script site-scraper que muestra el ciclo de eventos y las tareas en funcionamiento:

importar asyncio de web_scraping_library importar read_from_site_async tasks = [] async def main (url_list): for n in url_list: tasks.append (asyncio.create_task (read_from_site_async (n))) imprimir (tareas) return await asyncio (* tasks) urlsather = ['//sitio1.com','//otrositio.com','//noticias.com'] loop = asyncio.get_event_loop () results = loop.run_until_complete (main (urls)) print (results) 

Este script utiliza el bucle de eventos y los objetos de tarea de forma más explícita.

  • The .get_event_loop() method provides us with an object that lets us control the event loop directly, by submitting async functions to it programmatically via .run_until_complete(). In the previous script, we could only run a single top-level async function, using asyncio.run(). By the way, .run_until_complete() does exactly what it says: It runs all of the supplied tasks until they’re done, then returns their results in a single batch.
  • The .create_task() method takes a function to run, including its parameters, and gives us back a Task object to run it. Here we submit each URL as a separate Task to the event loop, and store the Task objects in a list. Note that we can only do this inside the event loop—that is, inside an async function.

How much control you need over the event loop and its tasks will depend on how complex the application is that you’re building. If you just want to submit a set of fixed jobs to run concurrently, as with our web scraper, you won’t need a whole lot of control—just enough to launch jobs and gather the results. 

By contrast, if you’re creating a full-blown web framework, you’ll want far more control over the behavior of the coroutines and the event loop. For instance, you may need to shut down the event loop gracefully in the event of an application crash, or run tasks in a threadsafe manner if you’re calling the event loop from another thread.

Async vs. threading vs. multiprocessing

At this point you may be wondering, why use async instead of threads or multiprocessing, both of which have been long available in Python?

First, there is a key difference between async and threads or multiprocessing, even apart from how those things are implemented in Python. Async is about concurrency, while threads and multiprocessing are about parallelism. Concurrency involves dividing time efficiently among multiple tasks at once—e.g., checking your email while waiting for a register at the grocery store. Parallelism involves multiple agents processing multiple tasks side by side—e.g., having five separate registers open at the grocery store.

Most of the time, async is a good substitute for threading as threading is implemented in Python. This is because Python doesn’t use OS threads but its own cooperative threads, where only one thread is ever running at a time in the interpreter. In comparison to cooperative threads, async provides some key advantages:

  • Async functions are far more lightweight than threads. Tens of thousands of asynchronous operations running at once will have far less overhead than tens of thousands of threads.
  • The structure of async code makes it easier to reason about where tasks pick up and leave off. This means data races and thread safety are less of an issue. Because all tasks in the async event loop run in a single thread, it’s easier for Python (and the developer) to serialize how they access objects in memory.
  • Async operations can be cancelled and manipulated more readily than threads. The Task object we get back from asyncio.create_task() provides us with a handy way to do this.

Multiprocessing in Python, on the other hand, is best for jobs that are heavily CPU-bound rather than I/O-bound. Async actually works hand-in-hand with multiprocessing, as you can use asyncio.run_in_executor() to delegate CPU-intensive jobs to a process pool from a central process, without blocking that central process.

Next steps with Python async

The best first thing to do is build a few, simple async apps of your own. Good examples abound now that asynchronous programming in Python has undergone a few versions and had a couple of years to settle down and become more widely used. The official documentation for asyncio is worth reading over to see what it offers, even if you don’t plan to make use of all of its functions.

También puede explorar el creciente número de bibliotecas y middleware con tecnología asíncrona, muchos de los cuales proporcionan versiones asincrónicas y sin bloqueo de conectores de bases de datos, protocolos de red y similares. El aio-libsrepositorio tiene algunas claves, como la aiohittpbiblioteca para acceso web. También vale la pena buscar bibliotecas con la asyncpalabra clave en el índice de paquetes de Python . Con algo como la programación asincrónica, la mejor manera de aprender es ver cómo otros la han utilizado.