Accueil > Boostez la performance de vos applications avec Asyncio : un guide pratique pour les développeurs Python
Fawzi Rida
27 avril 2023
Read this post in English

Boostez la performance de vos applications avec Asyncio : un guide pratique pour les développeurs Python

Boostez la performance de vos applications avec Asyncio : un guide pratique pour les développeurs Python

Actuellement, la plupart des applications se reposent fortement sur les opérations Input/Ouput (I/O). Ce type d’opération inclut le téléchargement du contenu d’une page web depuis Internet, la communication sur un réseau avec des microservices, ou l’exécution de plusieurs requêtes SQL contre une base de données. Effectuer plusieurs opérations I/O peut engendrer un temps de délai pouvant nuire drastiquement à la performance de nos applications. Cependant, si nous exploitons la concurrence, nous pouvons éviter ce genre de problème à nos applications.

Asyncio a été introduit dans Python 3.4 comme un moyen supplémentaire pour gérer des charges de travail hautement concurrentes en dehors du multithreading et multiprocessing.

 

Parallélisme vs Concurrence

 

Souvent, la concurrence et le parallélisme sont utilisés d’une manière interchangeable. Or ces deux concepts sont différents. Le parallélisme est l’aptitude à exécuter simultanément des tâches sur plusieurs processeurs distincts.

C’est une approche qui est utilisée pour des tâches intenses en calcul (CPU bound tasks). Tandis que la concurrence est la capacité à exécuter des tâches en même temps, dans un ordre quelconque. Il s’agit de la synchronisation des processus entre eux au travers d’une mémoire partagée sur un seul processeur. Cette approche est utilisée dans des opérations liées à l’I/O, où le temps d’attente pour une opération peut être utilisé pour exécuter d’autres tâches.

Illustration de la différence entre le parallélisme et la concurrence

Illustration de la différence entre le parallélisme et la concurrence

 

Présentation d’Asyncio

 

Quel problème Asyncio essaie-t-il de résoudre ?

 

Asyncio résout les problèmes de programmation asynchrone pour des charges de travails I/O . Il existe deux raisons pour préférer l’implémentation de la concurrence avec de la programmation asynchrone sur une concurrence à base de threads :

  • Asyncio offre une alternative plus sûre, évitant les bugs, les conditions de courses (race conditions) et autres dangers non déterministes qui se produisent fréquemment dans des applications threadées.
  • Asyncio offre un moyen simple de gérer simultanément des charges de plusieurs milliers de connexions de sockets, et supporte les connexions de longue durée telles que WebSockets ou MQTT pour des applications IoT.

 

Avant de continuer, il est important de clarifier certains concepts comme le Global Interpreter Lock (GIL), les conditions de courses (race conditions), Multitâche préemptif et Multitâche coopératif.

Le GIL rend le code de l’interpréteur Python thread-safe : c’est-à-dire qu’il permet le verrouillage de l’interpréteur pour garantir qu’un seul thread Python est en cours d’exécution à la fois, même si plusieurs threads sont actifs. Vous vous posez sûrement la question : pourquoi existe-t-il ? La réponse est la gestion de la mémoire dans CPython. Elle est principalement managée par une méthode qui s’appelle Reference Counting, qui permet la gestion automatique pour allouer ou libérer la mémoire d’un objet. Cette méthode consiste à allouer un bloc de mémoire à chaque objet, et un décompte de références est maintenu pour garder une trace du nombre de références à cet objet. Lorsque le compteur de références tombe à zéro, la mémoire de cet objet est automatiquement libérée.

Les conditions de courses se produisent quand deux threads doivent référencer un objet Python en même temps. Dans ce cas, on parle d’un code non thread-safe, ce qui conduit à des états erronés et inattendus.

Multitâche préemptif : c’est la capacité d’un système d’exploitation multitâche à suspendre une tâche au profit d’une autre.

Multitâche coopératif : dans ce modèle, au lieu de laisser l’OS décider de basculer d’une tâche vers une autre, on le fait d’une manière programmatique dans le code.

 

Quelques intox à propos de Asyncio

 

« Asyncio supprime les problèmes avec le GIL » : Ce n’est pas vrai, car que Asyncio n’est même pas affecté par le GIL. Asyncio est par définition single-threaded, et le GIL affecte surtout les programmes multithreaded.

« Asyncio empêche toutes les conditions de courses (race conditions) » : Faux. Dès lors que l’on fait de la programmation concurrente, on est toujours exposé à des conditions de courses. Néanmoins, Asyncio élimine les problèmes liés à l’accès à la mémoire partagée entre les processus, ce qui est un phénomène récurrent pour les threads.

« Asyncio rend la programmation concurrente simple et plus rapide que celle avec les threads » : Pas vraiment. Dans certains cas, les threads sont plus rapides, mais au moins avec Asyncio, on a plus de contrôle sur le code et on a uniquement un thread à gérer. Ce qui simplifie, entre autres, le debugging en cas de pépin.

 

Les composants d’Asyncio

 

A première vue, la documentation d’Asyncio peut être intimidante, mais ce qu’il faut savoir, c’est qu’il existe des APIs pour la conception de Frameworks, et d’autres pour le développement de tous les jours. Pour une utilisation quotidienne, on a un sous-ensemble d’APIs pour les tâches suivantes :

  • Lancement d’une asyncio event loop ;
  • Appeler des fonctions async/await ;
  • Création de tâches à lancer dans la event loop ;
  • Attendre la complémentation de toutes les tâches ;
  • Fermetures de l’event loop une fois toutes les tâches finies.

 

Les composants d’Asyncio

 

Event Loop

 

L’event loop est le cœur de la bibliothèque Asyncio en Python. Elle est responsable de la gestion et de l’exécution des tâches asynchrones et callbacks. En gros, cette boucle vérifie constamment les tâches disponibles à l’exécution. Lorsqu’il n’y a pas de tâches à exécuter, la boucle d’événements attend que de nouvelles tâches deviennent disponibles. Cela permet à plusieurs tâches d’être exécutées en parallèle, sans bloquer l’exécution du programme.

Event Loop

 

Coroutines

 

Une coroutine n’est rien qu’une fonction Python, mais qui peut mettre en pause son exécution quand elle fait face à une opération chronophage. Une fois l’opération finie, on peut « réveiller » notre coroutine en pause. Pour créer et pauser une coroutine, on doit utiliser les mots clés async et await

Coroutines

 

Les classes Tasks et Futures

 

La classe Future est une superclass de Task qui fournit des fonctionnalités spécifiques à l’interaction avec l’event loop. Pour mieux comprendre ces deux classes, faut considérer la classe Future comme un état de complétion futur d’une activité. Elle est gérée par l’event loop (si vous avez déjà utilisé JavaScript, ça représente une promesse « promise »). Alors que la classe Task est très similaire, mais spécifique à une activité qui est une coroutine qu’on a créée avec la fonction create_task().

La hiérarchie d’héritage de classe Awaitable.

La hiérarchie d’héritage de classe Awaitable

 

Exemple pratique

 

Exemple pratique 1

Exemple pratique 2

 

Discussion autour d’Asyncio

 

Dans l’exemple précédent, on voit clairement qu’Asyncio est 10 fois plus performant que le threading. Asyncio est plus rapide, car il peut basculer entre les coroutines plus efficacement que threading entre les threads.

Lorsqu’une coroutine appelle ‘await asyncio.sleep(1)’, elle renvoie le contrôle à l’event loop qui peut ensuite passer à une autre coroutine prête à être exécutée. Cela permet l’exécution de plusieurs coroutines simultanément sur un seul thread, sans coût de changement de contexte entre les threads.

En revanche, lorsqu’un thread appelle ‘time.sleep(1)’,il bloque l’ensemble du thread et empêche tout autre thread de s’exécuter pendant ce temps. Cela signifie que plusieurs threads doivent être créés et gérés, ce qui peut entrainer un coût important (d’où vient l’effet négatif du GIL sur le multithreading).

A noter : la performance d’Asyncio par rapport au threading dépendra du cas d’usage spécifique. Il peut y avoir des cas où threading est plus rapide ou plus approprié qu’Asyncio. Cependant, si on veut exécuter un grand nombre de tâches simultanément et sans blocage I/O, Asyncio peut être une solution plus efficace que threading.

 

Comparaison : Asyncio vs Threading

 

Asyncio Threading
Programmation événementielle (asynchrone) Programmation procédurale et OOP
Concurrence basée sur les coroutines Concurrence basée sur les threads
Plus léger que les threads Plus lourd que les coroutines
Pas de GIL Limité par GIL
Non blocking I/O Blocking I/O
>100.000 tâches 100 – 1000 tâches

 

 

Comment choisir entre Asyncio, threading ou multi processing ?

 

Problème lié à l’I/O (I/O Bound) : utiliser Asyncio s’il est supporté par les librairies utilisées. Si ce n’est pas le cas, utiliser le threading.

Problème lié au calcul intensif (CPU bound) : utiliser le multi-processing.

Opération Parallèle Mémoire partagée Arrêt possible ?
Asyncio I/O Non Oui Oui
Threading I/O Non Oui Non
Multiprocessing CPU Oui Non Oui

 

Vous souhaitez être accompagnés dans vos projets de transformation numérique ? Contactez-nous !

Offres d'emploi consultant Cloud Paris Lyon Nantes Cellenza

Nos autres articles
Commentaires
Laisser un commentaire

Restez au courant des dernières actualités !
Le meilleur de l’actualité sur le Cloud, le DevOps, l’IT directement dans votre boîte mail.