Principes de base de la programmation asynchrone/en attente avec des exemples Python

Principes de base de la programmation asynchrone/en attente avec des exemples Python

Ces dernières années, de nombreux langages de programmation se sont efforcés d’améliorer leurs primitives de concurrence. Go a des goroutines, Ruby a des fibres et, bien sûr, Node.js a contribué à populariser async/wait, qui est aujourd’hui le type d’opérateur de concurrence le plus répandu. Dans cet article, je parlerai des bases de async/wait, en utilisant Python comme exemple. J’ai choisi Python, car cette fonctionnalité est relativement récente dans Python 3 et de nombreux utilisateurs ne la connaissent peut-être pas encore (en particulier compte tenu du temps qu’il a fallu à Python 2.7 pour arriver en fin de vie).

La principale raison d’utiliser async/wait est d’améliorer le débit d’un programme en réduisant le temps d’inactivité lors de l’exécution d’E/S. Les programmes avec cet opérateur utilisent implicitement une abstraction appelée boucle d’événement pour jongler avec plusieurs chemins d’exécution en même temps. À certains égards, ces boucles d’événements ressemblent à la programmation multithread, mais une boucle d’événements vit normalement dans un seul thread. En tant que telle, elle ne peut pas effectuer plus d’un calcul à la fois. Pour cette raison, une boucle d’événements seule ne peut pas améliorer les performances des applications gourmandes en calculs. Cependant, cela peut considérablement améliorer les performances des programmes qui communiquent beaucoup sur le réseau, comme les applications connectées à une base de données Redis.

Chaque fois qu’un programme envoie une commande à Redis, il doit attendre que Redis formule une réponse et, si Redis est hébergé sur une autre machine, il y a aussi une latence du réseau. Une application simple à thread unique qui n’utilise pas de boucle d’événements reste inactive pendant qu’elle attend la réponse, ce qui gaspille beaucoup de cycles CPU. Gardez à l’esprit que la latence du réseau est mesurée en millisecondes, tandis que les instructions du processeur prennent des nanosecondes pour s’exécuter. C’est une différence de six ordres de grandeur.

À titre d’exemple, voici un exemple de code qui suit les gains pour un jeu hypothétique. Chaque entrée de flux contient le nom du gagnant et notre programme met à jour un ensemble trié Redis qui fait office de classement. Le code n’est pas très robuste, mais nous ne nous en soucions pas pour l’instant, car nous nous concentrons sur les performances du code bloquant par rapport au code non bloquant.

Pour écrire une version asynchrone équivalente du code ci-dessus, nous utiliserons aio-libs/aioredis.

La communauté aio-libs réécrit de nombreuses bibliothèques réseau Python pour inclure la prise en charge d’asyncio, l’implémentation de bibliothèque standard de Python d’une boucle d’événement. Voici une version non bloquante du code ci-dessus :

Ce code est essentiellement le même, à l’exception de quelques attendre mots-clés parsemés. La plus grande différence est ce qui se passe dans les deux dernières lignes. Dans Node.js, l’environnement charge une boucle d’événement par défaut, tandis qu’en Python, vous devez la démarrer explicitement – c’est ce que font ces dernières lignes.

Après la réécriture, nous pourrions penser que nous avons amélioré les performances simplement en faisant autant. Malheureusement, la version non bloquante de notre code n’améliore pas encore les performances. Le problème ici réside dans les détails de la façon dont nous avons écrit le code, pas dans l’idée générale d’utiliser async / await.

Limiter l’utilisation de l’attente

Le principal problème avec notre réécriture est que nous avons surutilisé attendre. Lorsque nous préfixons un appel asynchrone avec attendrenous faisons deux choses :

  1. Programmez-le pour l’exécution.
  2. Attendez qu’il soit terminé.

Parfois, c’est la bonne chose à faire. Par exemple, nous ne pourrons pas itérer sur chaque événement tant que nous n’aurons pas fini de lire le flux sur la ligne 15. Dans ce cas, le attendre mot-clé a du sens, mais regardez add_new_win:

Dans cette fonction, la deuxième opération ne dépend pas vraiment de la première. Nous serions d’accord pour que la deuxième commande soit envoyée avec la première, mais attendre bloque le flux d’exécution dès que nous envoyons le premier. Nous aimerions trouver un moyen de planifier les deux opérations immédiatement. Pour cela, nous avons besoin d’une primitive de synchronisation différente.

Tout d’abord, l’appel direct d’une fonction asynchrone n’exécutera aucun de ses codes. Au lieu de cela, il instanciera simplement une “tâche”. Selon la langue de votre choix, cela peut s’appeler coroutine, promesse, futur ou autre chose. Sans entrer dans les détails, pour nous, une tâche est un objet représentant une valeur qui ne sera disponible qu’après avoir utilisé attendre ou une autre primitive de synchronisation, comme asyncio.gather.

Dans la documentation officielle de Python, vous pouvez trouver plus d’informations sur asyncio.gather. En bref, cela nous permet de planifier plusieurs tâches en même temps. Nous devons le faire attendre son résultat car il crée une nouvelle tâche qui se termine une fois que toutes les tâches d’entrée sont terminées. Python asyncio.gather est équivalent à JavaScript Promesse.toutC# Tâche.QuandTousde Kotlin attendre toutetc.

Améliorer notre boucle principale

La même chose que nous avons faite pour add_new_win peut également être effectué pour la boucle de traitement des événements du flux principal. Voici le code auquel je fais référence :

Compte tenu de ce que nous avons appris jusqu’à présent, vous remarquerez que nous traitons chaque événement de manière séquentielle. Nous le savons avec certitude car à la ligne 6, l’utilisation de attendre les deux horaires et attend l’achèvement de add_new_win. Parfois, c’est exactement ce que vous voulez, car la logique du programme se briserait si vous appliquiez des modifications dans le désordre. Dans notre cas, nous ne nous soucions pas vraiment de la commande car nous ne faisons que mettre à jour les compteurs.

Nous traitons maintenant simultanément chaque lot d’événements, et notre changement de code a été minime. Une dernière chose à garder à l’esprit est que parfois les programmes peuvent être performants même sans l’utilisation de asyncio.gather. En particulier, lorsque vous écrivez du code pour un serveur Web et que vous utilisez un framework asynchrone comme Sanicle framework appellera vos gestionnaires de requêtes de manière simultanée, garantissant un débit élevé même si vous attendre chaque appel de fonction asynchrone.

En conclusion

Voici l’exemple de code complet après les deux changements que nous avons présentés ci-dessus :

Afin d’exploiter les E/S non bloquantes, vous devez repenser votre approche des opérations en réseau. La bonne nouvelle est que ce n’est pas particulièrement difficile, vous avez juste besoin de savoir quand la séquentialité est importante et quand elle ne l’est pas. Essayez d’expérimenter avec aioredis ou un client Redis asynchrone équivalent, et voyez dans quelle mesure vous pouvez améliorer le débit de vos applications.

Development Source

Related Posts

RLEC 4.2.1 apporte des contrôles granulaires à la haute disponibilité et aux performances

RLEC 4.2.1 apporte des contrôles granulaires à la haute disponibilité et aux performances

Comment HolidayMe utilise Redis Enterprise comme base de données principale

Comment HolidayMe utilise Redis Enterprise comme base de données principale

Annonce de RedisGears 1.0 : un moteur sans serveur pour Redis

Annonce de RedisGears 1.0 : un moteur sans serveur pour Redis

Clés Redis dans la RAM |  Redis

Clés Redis dans la RAM | Redis

No Comment

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *