Démarrez avec async en Python

La programmation asynchrone, ou asynchrone pour faire court, est une fonctionnalité de nombreux langages modernes qui permet à un programme de jongler avec plusieurs opérations sans attendre ni se raccrocher à l'une d'entre elles. C'est un moyen intelligent de gérer efficacement des tâches telles que les E / S réseau ou de fichiers, où la plupart du temps du programme est passé à attendre la fin d'une tâche.

Imaginez une application de scraping Web qui ouvre 100 connexions réseau. Vous pouvez ouvrir une connexion, attendre les résultats, puis ouvrir la suivante et attendre les résultats, et ainsi de suite. La plupart du temps que le programme s'exécute est passé à attendre une réponse du réseau, sans faire de travail réel.

Async vous offre une méthode plus efficace: ouvrez les 100 connexions à la fois, puis basculez entre chaque connexion active à mesure qu'elles renvoient des résultats. Si une connexion ne renvoie pas de résultats, passez à la suivante, et ainsi de suite, jusqu'à ce que toutes les connexions aient renvoyé leurs données.

La syntaxe async est désormais une fonctionnalité standard de Python, mais les pythonistes de longue date qui sont habitués à faire une chose à la fois peuvent avoir du mal à s'y retrouver. Dans cet article, nous allons explorer le fonctionnement de la programmation asynchrone en Python et comment l'utiliser.

Notez que si vous souhaitez utiliser async en Python, il est préférable d'utiliser Python 3.7 ou Python 3.8 (la dernière version à ce jour). Nous utiliserons la syntaxe asynchrone et les fonctions d'assistance de Python telles que définies dans ces versions du langage.

Quand utiliser la programmation asynchrone

En général, les meilleurs moments pour utiliser async sont lorsque vous essayez d'effectuer un travail présentant les caractéristiques suivantes:

  • Le travail prend beaucoup de temps.
  • Le délai implique l'attente d'opérations d'E / S (disque ou réseau), pas de calcul.
  • Le travail implique de nombreuses opérations d'E / S en même temps, ou une ou plusieurs opérations d'E / S se produisant lorsque vous essayez également d'effectuer d'autres tâches.

Async vous permet de configurer plusieurs tâches en parallèle et de les parcourir efficacement, sans bloquer le reste de votre application.

Quelques exemples de tâches qui fonctionnent bien avec async:

  • Grattage Web, comme décrit ci-dessus.
  • Services réseau (par exemple, un serveur Web ou une infrastructure).
  • Programmes qui coordonnent les résultats de plusieurs sources qui mettent du temps à renvoyer des valeurs (par exemple, des requêtes de base de données simultanées).

Il est important de noter que la programmation asynchrone est différente du multithreading ou du multiprocessing. Les opérations asynchrones s'exécutent toutes dans le même thread, mais elles se cèdent les unes aux autres si nécessaire, ce qui rend l'async plus efficace que le threading ou le multitraitement pour de nombreux types de tâches. (Plus d'informations ci-dessous.)

Python asyncawaitetasyncio

Python a récemment ajouté deux mots clés asyncet await, pour créer des opérations asynchrones. Considérez ce script:

def get_server_status (server_addr) # Une opération potentiellement longue ... return server_status def server_ops () results = [] results.append (get_server_status ('addr1.server') results.append (get_server_status ('addr2.server') return résultats 

Une version asynchrone du même script - non fonctionnelle, juste assez pour nous donner une idée du fonctionnement de la syntaxe - pourrait ressembler à ceci.

async def get_server_status (server_addr) # Une opération potentiellement longue ... return server_status async def server_ops () results = [] results.append (wait get_server_status ('addr1.server') results.append (wait get_server_status ('addr2. server ') renvoie les résultats 

Les fonctions précédées du asyncmot - clé deviennent des fonctions asynchrones, également appelées coroutines . Les coroutines se comportent différemment des fonctions régulières:

  • Les coroutines peuvent utiliser un autre mot-clé,, awaitqui permet à une coroutine d'attendre les résultats d'une autre coroutine sans blocage. Jusqu'à ce que les résultats reviennent de la awaitcoroutine ed, Python bascule librement entre les autres coroutines en cours d'exécution.
  • Les coroutines ne peuvent être appelées qu'à partir d'autres asyncfonctions. Si vous exécutez server_ops()ou get_server_status()tel quel à partir du corps du script, vous n'obtiendrez pas leurs résultats; vous obtiendrez un objet coroutine Python, qui ne peut pas être utilisé directement.

Donc, si nous ne pouvons pas appeler des asyncfonctions à partir de fonctions non asynchrones, et que nous ne pouvons pas exécuter des asyncfonctions directement, comment les utiliser? Réponse: En utilisant la asynciobibliothèque, quels ponts asyncet le reste de Python.

Python asyncawaitet asyncioexemple

Voici un exemple (encore une fois, non fonctionnel mais illustratif) de la façon dont on pourrait écrire une application de web scraping en utilisant asyncet asyncio. Ce script prend une liste d'URL et utilise plusieurs instances d'une asyncfonction à partir d'une bibliothèque externe ( read_from_site_async()) pour les télécharger et agréger les résultats.

import asyncio de web_scraping_library import read_from_site_async async def main (url_list): return wait asyncio.gather (* [read_from_site_async (_) for _ in url_list]) urls = ['//site1.com','//othersite.com', '//newsite.com'] results = asyncio.run (main (urls)) print (résultats) 

Dans l'exemple ci-dessus, nous utilisons deux asynciofonctions courantes :

  • asyncio.run()est utilisé pour lancer une asyncfonction à partir de la partie non asynchrone de notre code, et ainsi lancer toutes les activités asynchrones du programme. (C'est ainsi que nous courons main().)
  • asyncio.gather()prend une ou plusieurs fonctions décorées de manière asynchrone (dans ce cas, plusieurs instances de read_from_site_async()de notre hypothétique bibliothèque de web-scraping), les exécute toutes et attend que tous les résultats arrivent.

L'idée ici est de commencer l'opération de lecture pour tous les sites à la fois, puis de rassembler les résultats au fur et à mesure qu'ils arrivent (d'où asyncio.gather()). Nous n'attendons pas la fin d'une opération avant de passer à la suivante.

Composants des applications asynchrones Python

Nous avons déjà mentionné comment les applications asynchrones Python utilisent les coroutines comme ingrédient principal, en s'appuyant sur la asynciobibliothèque pour les exécuter. Quelques autres éléments sont également essentiels pour les applications asynchrones en Python:

Boucles d'événements

La asynciobibliothèque crée et gère les boucles d'événements , les mécanismes qui exécutent les coroutines jusqu'à ce qu'elles soient terminées. Une seule boucle d'événement doit être exécutée à la fois dans un processus Python, ne serait-ce que pour permettre au programmeur de garder une trace de ce qui s'y passe.

Tâches

Lorsque vous soumettez une coroutine à une boucle d'événements pour traitement, vous pouvez récupérer un Taskobjet, ce qui permet de contrôler le comportement de la coroutine depuis l'extérieur de la boucle d'événements. Si vous devez annuler la tâche en cours, par exemple, vous pouvez le faire en appelant la .cancel()méthode de la tâche .

Here is a slightly different version of the site-scraper script that shows the event loop and tasks at work:

import asyncio from web_scraping_library import 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))) print (tasks) return await asyncio.gather(*tasks) urls = ['//site1.com','//othersite.com','//newsite.com'] loop = asyncio.get_event_loop() results = loop.run_until_complete(main(urls)) print (results) 

This script uses the event loop and task objects more explicitly.

  • 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.

Vous pouvez également explorer le nombre croissant de bibliothèques et d'intergiciels asynchrones, dont beaucoup fournissent des versions asynchrones et non bloquantes de connecteurs de base de données, de protocoles réseau, etc. Le aio-libsréférentiel a quelques clés, telles que la aiohittpbibliothèque pour l'accès Web. Il vaut également la peine de rechercher dans l'index des packages Python les bibliothèques avec le asyncmot - clé. Avec quelque chose comme la programmation asynchrone, la meilleure façon d'apprendre est de voir comment les autres l'ont utilisée.