Débuter avec les fonctionnalités asynchrones en Python – Real Python

By | septembre 23, 2019

Expert Python

Avez-vous entendu parler de la programmation asynchrone en Python? Voulez-vous en savoir plus sur les fonctionnalités asynchrones de Python et sur leur utilisation dans votre travail? Peut-être avez-vous même essayé d'écrire des programmes en mode thread et rencontrez-vous des problèmes. Si vous souhaitez comprendre comment utiliser les fonctionnalités asynchrones de Python, vous êtes au bon endroit.

Dans cet article, vous apprendrez:

  • Quel programme synchrone est
  • Quel un programme asynchrone est
  • Pourquoi vouloir écrire un programme asynchrone
  • Comment utiliser les fonctionnalités asynchrones de Python

Tous les exemples de code de cet article ont été testés avec Python 3.7.2. Vous pouvez en obtenir une copie en cliquant sur le lien ci-dessous:

Comprendre la programmation asynchrone

UNE programme synchrone est exécuté une étape à la fois. Même avec des branchements, des boucles et des appels de fonction conditionnels, vous pouvez toujours penser au code en prenant une étape d'exécution à la fois. Lorsque chaque étape est terminée, le programme passe à la suivante.

Voici deux exemples de programmes qui fonctionnent de cette façon:

  • Programmes de traitement par lots sont souvent créés en tant que programmes synchrones. Vous obtenez une entrée, la traitez et créez une sortie. Les étapes se succèdent jusqu'à ce que le programme atteigne le résultat souhaité. Le programme doit seulement faire attention aux étapes et à leur ordre.

  • Programmes en ligne de commande sont de petits processus rapides exécutés dans un terminal. Ces scripts sont utilisés pour créer quelque chose, transformer une chose en autre chose, générer un rapport ou peut-être lister des données. Cela peut être exprimé sous la forme d'une série d'étapes de programme exécutées séquentiellement jusqu'à la fin du programme.

Un programme asynchrone se comporte différemment. Il faut encore une étape d'exécution à la fois. La différence est que le système ne peut pas attendre la fin d'une étape d'exécution avant de passer à l'étape suivante.

Cela signifie que le programme passera aux étapes d’exécution futures, même si une étape précédente n’a pas encore aboutie et s’exécute toujours ailleurs. Cela signifie également que le programme sait quoi faire lorsqu'une étape précédente est terminée.

Pourquoi voudriez-vous écrire un programme de cette manière? La suite de cet article vous aidera à répondre à cette question et vous donnera les outils nécessaires pour résoudre avec élégance des problèmes asynchrones intéressants.

Construire un serveur Web synchrone

L’unité de travail de base d’un serveur Web correspond plus ou moins au traitement par lots. Le serveur obtiendra une entrée, la traitera et créera la sortie. Écrit en tant que programme synchrone, cela créerait un serveur Web opérationnel.

Ce serait aussi un absolument terrible serveur Web.

Pourquoi? Dans ce cas, une unité de travail (entrée, processus, sortie) n’est pas le seul objectif. Le but réel est de traiter des centaines voire des milliers d'unités de travail le plus rapidement possible. Cela peut se produire sur de longues périodes et plusieurs unités de travail peuvent même arriver toutes en même temps.

Peut-on améliorer un serveur Web synchrone? Bien sûr, vous pouvez optimiser les étapes d’exécution de sorte que tout le travail entrant soit traité le plus rapidement possible. Malheureusement, cette approche présente des limites. Le résultat pourrait être un serveur Web qui ne répond pas assez rapidement, qui ne peut pas gérer suffisamment de travail ou même un serveur dont le délai de péremption est dépassé.

Dans un programme synchrone, si une étape d'exécution lance une requête de base de données, la CPU est essentiellement inactive jusqu'à ce que la requête de base de données soit renvoyée. Pour les programmes en mode batch, ce n’est pas une priorité la plupart du temps. Le traitement des résultats de cette opération IO est l'objectif. Cela peut souvent prendre plus de temps que l'opération IO elle-même. Tous les efforts d'optimisation seraient concentrés sur le travail de traitement, pas sur les entrées-sorties.

Les techniques de programmation asynchrone permettent à vos programmes de tirer parti de processus d'E / S relativement lents en libérant le processeur pour qu'il puisse effectuer d'autres tâches.

Penser différemment à propos de la programmation

Lorsque vous commencez à essayer de comprendre la programmation asynchrone, vous pouvez voir beaucoup de discussions sur l’importance de blocageou écrit code non bloquant. (Personnellement, j'ai eu du mal à comprendre ces concepts des personnes à qui j'ai posé la question et de la documentation que j'ai lue.)

Qu'est-ce qu'un code non bloquant? Qu'est-ce qui bloque le code, d'ailleurs? Les réponses à ces questions vous aideraient-elles à écrire un meilleur serveur Web? Si oui, comment pourriez-vous le faire? Découvrons-le!

L'écriture de programmes asynchrones nécessite que vous pensiez différemment à la programmation. Bien que cette nouvelle façon de penser puisse être difficile à comprendre, c’est aussi un exercice intéressant. C’est parce que le monde réel est presque entièrement asynchrone, tout comme la façon dont vous interagissez avec lui.

Imaginez ceci: vous êtes un parent qui essaie de faire plusieurs choses à la fois. Vous devez équilibrer le chéquier, faire la lessive et garder un œil sur les enfants. D'une manière ou d'une autre, vous êtes capable de faire toutes ces choses en même temps sans même y penser! Décrivons-le:

  • Équilibrer le chéquier est un synchrone tâche. Une étape en suit une autre jusqu'à ce que ce soit fait. Vous faites tout le travail vous-même.

  • Cependant, vous pouvez vous libérer du chéquier pour faire la lessive. Vous déchargez la sécheuse, déplacez les vêtements de la laveuse vers la sécheuse et lancez une autre charge dans la laveuse.

  • Travailler avec la laveuse et la sécheuse est une tâche synchrone, mais l'essentiel du travail se fait après la laveuse et la sécheuse sont démarrées. Une fois que vous les avez lancés, vous pouvez vous en aller et revenir à la tâche du carnet de chèques. À ce stade, les tâches de la laveuse et de la sécheuse sont devenues asynchrone. La laveuse et la sécheuse fonctionneront indépendamment jusqu'à ce que l'avertisseur sonore se déclenche (vous avertissant que la tâche nécessite de l'attention).

  • Regarder vos enfants est une autre tâche asynchrone. Une fois qu'ils sont installés et qu'ils jouent, ils peuvent le faire de manière indépendante pour la plupart. Cela change quand quelqu'un a besoin d'attention, comme quand quelqu'un a faim ou est blessé. Lorsque l'un de vos enfants crie en alarme, vous réagissez. Les enfants sont une tâche de longue haleine avec une priorité élevée. Les regarder remplace toutes les autres tâches que vous pourriez faire, comme le chéquier ou la lessive.

Ces exemples peuvent aider à illustrer les concepts de code bloquant et non bloquant. Pensons à cela en termes de programmation. Dans cet exemple, vous êtes comme le processeur. Pendant que vous déplacez le linge, vous (le processeur) êtes occupé et bloqué de faire un autre travail, comme équilibrer le chéquier. Mais ça va parce que la tâche est relativement rapide.

D'autre part, le démarrage de la laveuse et de la sécheuse ne vous empêche pas d'effectuer d'autres tâches. C’est une fonction asynchrone, car il n’est pas nécessaire d’attendre qu’elle se termine. Une fois que cela a commencé, vous pouvez revenir à autre chose. Ceci s'appelle un changement de contexte: le contexte de ce que vous faites a changé et le buzzer de la machine vous avertira à l’avenir lorsque la tâche de blanchisserie sera terminée.

En tant qu'être humain, c'est comme ça que vous travaillez tout le temps. Naturellement, vous jonglez avec plusieurs choses en même temps, souvent sans y penser. En tant que développeur, le truc consiste à traduire ce type de comportement en un code faisant la même chose.

Programmer les parents: pas si facile que ça en a l'air!

Si vous vous reconnaissez (ou vos parents) dans l'exemple ci-dessus, alors c'est génial! Vous avez une longueur d'avance dans la compréhension de la programmation asynchrone. Encore une fois, vous pouvez passer assez facilement d’un contexte à l’autre, en sélectionnant certaines tâches et en reprenant d’autres. Vous allez maintenant essayer de programmer ce comportement dans des parents virtuels!

Expérience de pensée n ° 1: le parent synchrone

Comment voulez-vous créer un programme parent pour effectuer les tâches ci-dessus de manière complètement synchrone? Étant donné que surveiller les enfants est une tâche hautement prioritaire, votre programme ferait peut-être cela. Le parent veille sur les enfants en attendant que quelque chose se passe qui puisse nécessiter leur attention. Cependant, rien d'autre (comme le chéquier ou la lessive) ne serait fait dans ce scénario.

Maintenant, vous pouvez redéfinir la priorité des tâches comme vous le souhaitez, mais une seule d'entre elles se produirait à un moment donné. C’est le résultat d’une approche synchrone pas à pas. Comme le serveur Web synchrone décrit ci-dessus, cela fonctionnerait, mais ce ne serait peut-être pas la meilleure façon de vivre. Le parent ne serait pas en mesure d’effectuer d’autres tâches tant que les enfants ne se seraient pas endormis. Toutes les autres tâches se dérouleraient ensuite, tard dans la nuit. (Dans quelques semaines, de vrais parents risquent de sauter par la fenêtre!)

Expérience de pensée n ° 2: le parent votant

Si vous avez utilisé vote, vous pouvez alors changer les choses afin que plusieurs tâches soient terminées. Dans cette approche, le parent s'éloignait périodiquement de la tâche en cours et vérifiait si d'autres tâches devaient faire l'objet d'une attention particulière.

Faisons l’intervalle d’interrogation d’environ quinze minutes. Maintenant, toutes les quinze minutes, votre parent vérifie si la laveuse, la sécheuse ou les enfants ont besoin d'attention. Si ce n'est pas le cas, le parent peut retourner travailler sur le chéquier. Cependant, si l’une de ces tâches faire besoin d'attention, alors le parent s'en occupera avant de retourner au chéquier. Ce cycle se poursuit jusqu'à la prochaine expiration de la boucle d'interrogation.

Cette approche fonctionne également car plusieurs tâches retiennent l'attention. Cependant, il y a quelques problèmes:

  1. Le parent peut passer beaucoup de temps à vérifier des choses qui n’ont pas besoin d’attention: La laveuse et la sécheuse ne sont pas encore terminées et les enfants n’ont besoin d’attention à moins que quelque chose d’inattendu se produise.

  2. Le parent peut manquer des tâches terminées qui nécessitent une attention particulière: Par exemple, si la laveuse a terminé son cycle au début de l’intervalle d’interrogation, elle n’aura aucune attention pendant quinze minutes! De plus, regarder les enfants est censé être la tâche la plus prioritaire. Ils ne pouvaient pas tolérer quinze minutes sans aucune attention lorsque quelque chose pouvait mal se passer.

Vous pouvez résoudre ces problèmes en réduisant l'intervalle d'interrogation, mais votre parent (le processeur) consacrerait plus de temps au changement de contexte dans le temps entre les tâches. C'est à ce moment que vous commencez à atteindre un point de rendement décroissant. (Encore une fois, quelques semaines comme ça et bien… Voir le commentaire précédent sur les fenêtres et le saut.)

Expérience de pensée n ° 3: le parent qui enfile

"Si seulement je pouvais me cloner …" Si vous êtes un parent, alors vous avez probablement eu des pensées similaires! Puisque vous programmez des parents virtuels, vous pouvez le faire essentiellement en utilisant filetage. C'est un mécanisme qui permet à plusieurs sections d'un programme de s'exécuter en même temps. Chaque section de code exécutée indépendamment est connue sous le nom de filet tous les threads partagent le même espace mémoire.

Si vous pensez que chaque tâche fait partie d'un programme, vous pouvez les séparer et les exécuter en tant que threads. En d’autres termes, vous pouvez «cloner» le parent en créant une instance pour chaque tâche: surveiller les enfants, surveiller la laveuse, surveiller la sécheuse et équilibrer le chéquier. Tous ces «clones» fonctionnent indépendamment.

Cela semble être une très bonne solution, mais il y a également quelques problèmes. La première est que vous devrez explicitement indiquer à chaque instance parent ce qu’il faut faire dans votre programme. Cela peut entraîner des problèmes car toutes les instances partagent tout dans l’espace programme.

Par exemple, supposons que le parent A surveille le séchoir. Le parent A voit que les vêtements sont secs, il prend donc le contrôle de la sécheuse et commence à décharger les vêtements. En même temps, le parent B voit que la laveuse est terminée, il en prend le contrôle et commence à enlever les vêtements. Cependant, le parent B doit également prendre le contrôle de la sécheuse pour pouvoir y mettre les vêtements mouillés. Cela ne peut pas arriver, car le parent A contrôle actuellement la sécheuse.

Après un moment, le parent A a fini de décharger ses vêtements. Maintenant, ils veulent prendre le contrôle de la laveuse et commencer à déplacer les vêtements dans la sécheuse vide. Cela ne peut pas arriver non plus, car le parent B a actuellement le contrôle de la laveuse!

Ces deux parents sont maintenant impasse. Les deux ont le contrôle de leurs propres ressources et veulent le contrôle de l'autre ressource. Ils vont attendre indéfiniment que l’autre instance parent libère le contrôle. En tant que programmeur, vous devez écrire du code pour résoudre ce problème.

Voici un autre problème pouvant découler du threading. Supposons qu'un enfant soit blessé et doive être pris en charge d'urgence. Le parent C s'est vu confier la tâche de surveiller les enfants, alors ils l'emmenent immédiatement. Aux soins urgents, le parent C doit faire un chèque assez volumineux pour couvrir les frais de consultation.

Pendant ce temps, le parent D travaille à la maison sur son carnet de chèques. Ils ne sont pas au courant de l’enregistrement de ce gros chèque; ils sont donc très surpris que le compte courant familial soit soudainement à découvert;

N'oubliez pas que ces deux instances parent travaillent dans le même programme. Le compte courant familial est un ressource partagée, vous devez donc trouver un moyen pour le parent surveillant les enfants d’en informer le parent en charge du chéquier. Sinon, vous devrez fournir un mécanisme de verrouillage afin que la ressource de chéquier ne puisse être utilisée que par un seul parent à la fois, avec mises à jour.

Utilisation pratique des fonctionnalités asynchrones de Python

Vous allez maintenant adopter certaines des approches décrites dans les expériences de pensée ci-dessus et les transformer en programmes Python opérationnels.

Tous les exemples de cet article ont été testés avec Python 3.7.2. le exigences.txt fichier indique les modules à installer pour exécuter tous les exemples. Si vous n'avez pas encore téléchargé le fichier, vous pouvez le faire maintenant:

Vous voudrez peut-être également configurer un environnement virtuel Python pour exécuter le code afin de ne pas interférer avec votre système Python.

Programmation Synchrone

Ce premier exemple montre une manière assez artificielle de récupérer une tâche dans une file d'attente et de traiter ce travail. Une file d’attente en Python est une belle structure de données FIFO (premier entré premier sorti). Il fournit des méthodes pour mettre des objets en file d'attente et les extraire à nouveau dans l'ordre dans lequel ils ont été insérés.

Dans ce cas, le travail consiste à extraire un nombre de la file d'attente et à avoir un nombre de boucles allant jusqu'à ce nombre. Il imprime sur la console lorsque la boucle commence et affiche à nouveau le total. Ce programme montre comment plusieurs tâches synchrones peuvent traiter le travail dans une file d'attente.

Le programme nommé exemple_1.py dans le référentiel est répertorié dans son intégralité ci-dessous:

    1 importation queue
    2 
    3 def tâche(Nom, work_queue):
    4     si work_queue.vide():
    5         impression(F"Tâche Nom    rien à faire")
    6     autre:
    sept         tandis que ne pas work_queue.vide():
    8             compter = work_queue.obtenir()
    9             total = 0
dix             impression(F"Tâche Nom    fonctionnement")
11             pour X dans intervalle(compter):
12                 total + = 1
13             impression(F"Tâche Nom    total: total")
14 
15 def principale():
16     "" "
17                 C'est le point d'entrée principal du programme.
18                 "" "
19     # Créer la file d'attente de 'travail'
20     work_queue = queue.Queue()
21 
22     # Mettre du "travail" dans la file d'attente
23     pour travail dans [[[[15, dix, 5, 2]:
24         work_queue.mettre(travail)
25 
26     # Créer des tâches synchrones
27     les tâches = [[[[
28         (tâche, "Un", work_queue),
29         (tâche, "Deux", work_queue)
30     ]
31 
32     # Exécuter les tâches
33     pour t, n, q dans les tâches:
34         t(n, q)
35 
36 si __Nom__ == "__principale__":
37     principale()

Voyons ce que chaque ligne fait:

  • Ligne 1 importe le queue module. C'est ici que les magasins de programmes doivent être exécutés par les tâches.
  • Lignes 3 à 13 définir tâche(). Cette fonction tire le travail de work_queue et traite le travail jusqu’à ce qu’il n’y ait plus rien à faire.
  • Ligne 15 définit principale() pour exécuter les tâches du programme.
  • Ligne 20 crée le work_queue. Toutes les tâches utilisent cette ressource partagée pour récupérer du travail.
  • Lignes 23 à 24 mettre du travail dans work_queue. Dans ce cas, il s’agit d’un nombre aléatoire de valeurs à traiter par les tâches.
  • Lignes 27 à 29 créer une liste de tuples de tâches, avec les valeurs de paramètre, ces tâches seront transmises.
  • Lignes 33 à 34 parcourez la liste des tuples de tâches, appelez chacun d'eux et transmettez les valeurs de paramètre précédemment définies.
  • Ligne 36 appels principale() pour exécuter le programme.

La tâche dans ce programme est juste une fonction acceptant une chaîne et une file d'attente en tant que paramètres. Lorsqu'il est exécuté, il recherche tout ce qui se trouve dans la file d'attente à traiter. S'il y a du travail à faire, il extrait les valeurs de la file d'attente, lance une pour boucle pour compter jusqu’à cette valeur et affiche le total à la fin. Il continue de travailler en file d'attente jusqu'à ce qu'il ne reste plus rien et il s'en va.

Lorsque ce programme est exécuté, il produit le résultat présenté ci-dessous:

Première tâche en cours d'exécution
Tâche un total: 15
Première tâche en cours d'exécution
Tâche un total: 10
Première tâche en cours d'exécution
Tâche un total: 5
Première tâche en cours d'exécution
Tâche un total: 2
Deuxième tâche rien à faire

Cela montre que Première tâche fait tout le travail. le tandis que boucle qui Première tâche frappe à l'intérieur tâche() consomme tout le travail de la file d'attente et la traite. Quand cette boucle se termine, Deuxième tâche obtient une chance de courir. Cependant, il trouve que la file d'attente est vide, donc Deuxième tâche affiche une déclaration indiquant que cela n'a rien à voir, puis se ferme. Rien dans le code ne permet aux deux Première tâche et Deuxième tâche pour changer de contexte et travailler ensemble.

Concurrence coopérative simple

La prochaine version du programme permet aux deux tâches de travailler ensemble. Ajout d'un rendement instruction signifie que la boucle donnera le contrôle au point spécifié tout en maintenant son contexte. De cette façon, la tâche à exécuter peut être redémarrée plus tard.

le rendement déclaration tourne tâche() dans une Générateur. Une fonction génératrice est appelée comme toute autre fonction en Python, mais lorsque le rendement Si l'instruction est exécutée, le contrôle est renvoyé à l'appelant de la fonction. Il s’agit essentiellement d’un changement de contexte car la commande passe de la fonction de générateur à l’appelant.

La partie intéressante est que le contrôle peut être donné retour à la fonction de générateur en appelant suivant() sur le générateur. Il s’agit d’un changement de contexte vers la fonction générateur, qui reprend l’exécution avec toutes les variables de fonction définies avant le début de la procédure. rendement encore intact.

le tandis que boucle dans principale() profite de cela quand il appelle suivant). Cette instruction relance la tâche au point où elle avait précédemment été rendue. Tout cela signifie que vous êtes en contrôle lorsque le changement de contexte se produit: lorsque le rendement déclaration est exécutée dans tâche().

Ceci est une forme de multitâche coopératif. Le programme cède le contrôle de son contexte actuel pour que quelque chose d'autre puisse fonctionner. Dans ce cas, il permet au tandis que boucle dans principale() pour exécuter deux instances de tâche() en tant que fonction génératrice. Chaque instance utilise le travail de la même file d'attente. C’est un peu intelligent, mais c’est aussi beaucoup de travail pour obtenir les mêmes résultats que lors du premier programme. Le programme exemple_2.py illustre cette concurrence simple et est répertorié ci-dessous:

    1 importation queue
    2 
    3 def tâche(Nom, queue):
    4     tandis que ne pas queue.vide():
    5         compter = queue.obtenir()
    6         total = 0
    sept         impression(F"Tâche Nom    fonctionnement")
    8         pour X dans intervalle(compter):
    9             total + = 1
dix             rendement
11         impression(F"Tâche Nom    total: total")
12 
13 def principale():
14     "" "
15                 C'est le point d'entrée principal du programme.
16                 "" "
17     # Créer la file d'attente de 'travail'
18     work_queue = queue.Queue()
19 
20     # Mettre du "travail" dans la file d'attente
21     pour travail dans [[[[15, dix, 5, 2]:
22         work_queue.mettre(travail)
23 
24     # Créer des tâches
25     les tâches = [[[[
26         tâche("Un", work_queue),
27         tâche("Deux", work_queue)
28     ]
29 
30     # Exécuter les tâches
31     terminé = Faux
32     tandis que ne pas terminé:
33         pour t dans les tâches:
34             essayer:
35                 suivant(t)
36             sauf StopIteration:
37                 les tâches.retirer(t)
38             si len(les tâches) == 0:
39                 terminé = Vrai
40 
41 si __Nom__ == "__principale__":
42     principale()

Voici ce qui se passe dans le code ci-dessus:

  • Lignes 3 à 11 définir tâche() comme auparavant, mais l'ajout de rendement sur la ligne 10 transforme la fonction en générateur. Ceci où le changement de contexte est fait et le contrôle est rendu à la tandis que boucle dans principale().
  • Lignes 25 à 28 créer la liste des tâches, mais d'une manière légèrement différente de celle décrite dans l'exemple de code précédent. Dans ce cas, chaque tâche est appelée avec ses paramètres tels qu’ils ont été entrés dans la liste. les tâches liste variable. Ceci est nécessaire pour obtenir le tâche() fonction de générateur fonctionnant la première fois.
  • Lignes 34 à 39 sont les modifications à la tandis que boucle dans principale() qui permettent tâche() courir en coopération. C’est là que le contrôle revient à chaque instance de tâche() quand il cède, permettant à la boucle de continuer et d'exécuter une autre tâche.
  • Ligne 35 redonne le contrôle à tâche()et continue son exécution après le point où rendement a été appelé.
  • Ligne 39 définit le terminé variable. le tandis que la boucle se termine lorsque toutes les tâches sont terminées et supprimées les tâches.

Voici la sortie produite lorsque vous exécutez ce programme:

Première tâche en cours d'exécution
Tâche deux en cours d'exécution
Deuxième tâche total: 10
Tâche deux en cours d'exécution
Tâche un total: 15
Première tâche en cours d'exécution
Deuxième tâche total: 5
Tâche un total: 2

Vous pouvez voir que les deux Première tâche et Deuxième tâche exécutez et utilisez le travail de la file d'attente. C’est ce qui est prévu, car les deux tâches sont en cours de traitement et chacune est responsable de deux éléments de la file d’attente. C'est intéressant, mais encore une fois, il faut beaucoup de travail pour atteindre ces résultats.

Le truc ici utilise le rendement déclaration, qui tourne tâche() dans un générateur et effectue un changement de contexte. Le programme utilise ce commutateur de contexte pour donner le contrôle à la tandis que boucle dans principale(), permettant à deux instances d’une tâche de s’exécuter de manière coopérative.

Remarquez comment Deuxième tâche sort son premier total. Cela peut vous amener à penser que les tâches s'exécutent de manière asynchrone. Cependant, il s'agit toujours d'un programme synchrone. Il est structuré de manière à ce que les deux tâches puissent échanger des contextes. La raison pour laquelle Deuxième tâche son premier résultat total est qu’il ne compte que 10, alors que Première tâche compte jusqu'à 15. Deuxième tâche arrive tout simplement à son premier total, donc il doit imprimer sa sortie sur la console avant Première tâche.

Coopération simultanée avec blocage d'appels

La prochaine version du programme est la même que la dernière, à l’exception de l’ajout de time.sleep (delay) dans le corps de votre boucle de tâches. Cela ajoute un délai basé sur la valeur extraite de la file d'attente de travail à chaque itération de la boucle de tâches. Le délai est ajouté pour simuler l’effet d’une blocage d'appel se produisant dans votre tâche.

Un appel bloquant est un code qui empêche la CPU de faire autre chose pendant un certain temps. Dans les expériences de pensée ci-dessus, si un parent n’était pas en mesure de rompre l’équilibre du chéquier tant qu’il n’était pas complet, c’était alors un appel bloquant.

time.sleep (delay) fait la même chose dans cet exemple, car le processeur ne peut rien faire d’autre que attendre l’expiration du délai.

temps écoulé fournit un moyen d’obtenir le temps écoulé à partir du moment où une instance de la classe est créée jusqu’à ce qu’elle soit appelée en tant que fonction. Le programme exemple_3.py est énuméré ci-dessous:

    1 importation temps
    2 importation queue
    3 de lib.elapsed_time importation ET
    4 
    5 def tâche(Nom, queue):
    6     tandis que ne pas queue.vide():
    sept         retard = queue.obtenir()
    8         et = ET()
    9         impression(F"Tâche Nom    fonctionnement")
dix         temps.dormir(retard)
11         impression(F"Tâche Nom    temps total écoulé: et () :. 1f ")
12         rendement
13 
14 def principale():
15     "" "
16                 C'est le point d'entrée principal du programme.
17                 "" "
18     # Créer la file d'attente de 'travail'
19     work_queue = queue.Queue()
20 
21     # Mettre du "travail" dans la file d'attente
22     pour travail dans [[[[15, dix, 5, 2]:
23         work_queue.mettre(travail)
24 
25     les tâches = [[[[
26         tâche("Un", work_queue),
27         tâche("Deux", work_queue)
28     ]
29 
30     # Exécuter les tâches
31     et = ET()
32     terminé = Faux
33     tandis que ne pas terminé:
34         pour t dans les tâches:
35             essayer:
36                 suivant(t)
37             sauf StopIteration:
38                 les tâches.retirer(t)
39             si len(les tâches) == 0:
40                 terminé = Vrai
41 
42     impression(F" nTemps total écoulé: et ():. 1f ")
43 
44 si __Nom__ == "__principale__":
45     principale()

Voici ce qui est différent dans le code ci-dessus:

  • Ligne 1 importe le temps module pour donner au programme l'accès à le sommeil de temps().
  • Ligne 11 changements tâche() inclure un time.sleep (delay) imiter un délai IO. Ceci remplace le pour boucle qui a fait le décompte exemple_1.py.

Lorsque vous exécutez ce programme, vous verrez la sortie suivante:

Première tâche en cours d'exécution
Tâche 1 Temps total écoulé: 15.0
Tâche deux en cours d'exécution
Tâche deux temps total écoulé: 10.0
Première tâche en cours d'exécution
Tâche 1 Temps total écoulé: 5.0
Tâche deux en cours d'exécution
Tâche deux temps total écoulé: 2.0

Temps total écoulé: 32.01021909713745

Comme avant, les deux Première tâche et Deuxième tâche sont en cours d’exécution, utilisent le travail de la file et le traitent. Cependant, même avec l’ajout du délai, vous pouvez voir que la concurrence simultanée ne vous a rien apporté. Le délai arrête le traitement de tout le programme et la CPU attend simplement que le délai IO soit écoulé.

C’est exactement ce que l’on entend par code de blocage dans la documentation async Python. Vous remarquerez que le temps nécessaire à l’exécution de l’ensemble du programme correspond simplement à la durée cumulée de tous les retards. Exécuter des tâches de cette façon n'est pas une victoire.

Coopération simultanée avec des appels non bloquants

La prochaine version du programme a été modifiée un peu. Il utilise les fonctionnalités asynchrones de Python en utilisant asyncio / wait fournies dans Python 3.

le temps et queue les modules ont été remplacés par le asyncio paquet. Cela donne à votre programme l’accès à la fonctionnalité de mise en veille et de file d’attente asynchrone (non bloquante). Le changement de tâche() le définit comme asynchrone avec l'ajout du async préfixe sur la ligne 4. Cela indique à Python que la fonction sera asynchrone.

L’autre grand changement consiste à supprimer le time.sleep (delay) et rendement déclarations, et les remplacer par wait asyncio.sleep (delay). Cela crée un délai non bloquant qui effectuera un changement de contexte vers l'appelant principale().

le tandis que boucle à l'intérieur principale() n'existe plus. Au lieu de task_array, il y a un appel à attendez asyncio.gather (...). Cela raconte asyncio deux choses:

  1. Créez deux tâches basées sur tâche() et commencez à les exécuter.
  2. Attendez que les deux soient terminés avant d'aller de l'avant.

La dernière ligne du programme asyncio.run (main ()) court principale(). Cela crée ce que l’on appelle une boucle d’événement). C’est cette boucle qui se déroulera principale(), qui à son tour exécutera les deux instances de tâche().

La boucle d'événement est au cœur du système asynchrone Python. Il exécute tout le code, y compris principale(). Lorsque le code de tâche est en cours d'exécution, la CPU est en train de travailler. Quand le attendre le mot clé est atteint, un changement de contexte se produit et le contrôle repasse à la boucle d'événements. La boucle d’événement examine toutes les tâches en attente d’un événement (dans ce cas, un asyncio.sleep (délai) timeout) et passe le contrôle à une tâche avec un événement prêt.

wait asyncio.sleep (delay) est non bloquant en ce qui concerne la CPU. Au lieu d'attendre l'expiration du délai, la CPU enregistre un événement de veille dans la file d'attente des tâches de la boucle d'événements et effectue un changement de contexte en passant le contrôle à la boucle d'événements. La boucle d'événements recherche en permanence les événements terminés et redonne le contrôle à la tâche en attente de cet événement. De cette manière, la CPU peut rester occupée si du travail est disponible, tandis que la boucle d'événements surveille les événements à venir.

le exemple_4.py le code est listé ci-dessous:

    1 importation asyncio
    2 de lib.elapsed_time importation ET
    3 
    4 async def tâche(Nom, work_queue):
    5     tandis que ne pas work_queue.vide():
    6         retard = attendre work_queue.obtenir()
    sept         et = ET()
    8         impression(F"Tâche Nom    fonctionnement")
    9         attendre asyncio.dormir(retard)
dix         impression(F"Tâche Nom    temps total écoulé: et () :. 1f ")
11 
12 async def principale():
13     "" "
14                 C'est le point d'entrée principal du programme.
15                 "" "
16     # Créer la file d'attente de 'travail'
17     work_queue = asyncio.Queue()
18 
19     # Mettre du "travail" dans la file d'attente
20     pour travail dans [[[[15, dix, 5, 2]:
21         attendre work_queue.mettre(travail)
22 
23     # Exécuter les tâches
24     et = ET()
25     attendre asyncio.recueillir(
26         asyncio.create_task(tâche("Un", work_queue)),
27         asyncio.create_task(tâche("Deux", work_queue)),
28     )
29     impression(F" nTemps total écoulé: et ():. 1f ")
30 
31 si __Nom__ == "__principale__":
32     asyncio.courir(principale())

Voici ce qui est différent entre ce programme et exemple_3.py:

  • Ligne 1 importations asyncio pour accéder aux fonctionnalités asynchrones de Python. Ceci remplace le temps importation.
  • Ligne 4 montre l'ajout de la async mot-clé devant le tâche() définition. Ceci informe le programme que tâche peut fonctionner de manière asynchrone.
  • Ligne 9 remplace time.sleep (delay) avec le non bloquant asyncio.sleep (délai), qui renvoie également le contrôle (ou ramène les contextes) à la boucle d’événement principale.
  • Ligne 17 crée le mode asynchrone non bloquant work_queue.
  • Lignes 20 à 21 mettre du travail dans work_queue de manière asynchrone en utilisant le attendre mot-clé.
  • Lignes 25 à 28 créez les deux tâches et rassemblez-les afin que le programme attende que les deux tâches soient terminées.
  • Ligne 32 démarre le programme en cours d'exécution de manière asynchrone. Il commence également la boucle d'événement interne.

Lorsque vous regardez la sortie de ce programme, notez comment les deux Première tâche et Deuxième tâche commencez au même moment, puis attendez à la fausse communication IO:

Première tâche en cours d'exécution
Tâche deux en cours d'exécution
Tâche deux temps total écoulé: 10.0
Tâche deux en cours d'exécution
Tâche 1 Temps total écoulé: 15.0
Première tâche en cours d'exécution
Tâche deux temps total écoulé: 5.0
Tâche 1: temps total écoulé: 2.0

Temps total écoulé: 17,0

Cela indique que wait asyncio.sleep (delay) est non bloquant, et que d'autres travaux sont en cours.

À la fin du programme, vous remarquerez que le temps total écoulé est essentiellement la moitié du temps nécessaire pour exemple_3.py courir. C'est l'avantage d'un programme qui utilise les fonctionnalités asynchrones de Python! Chaque tâche était capable de courir wait asyncio.sleep (delay) en même temps. Le temps total d'exécution du programme est maintenant inférieur à la somme de ses parties. Vous vous êtes détaché du modèle synchrone!

Appels HTTP synchrones (bloquants)

La prochaine version du programme constitue à la fois un pas en avant et un pas en arrière. Le programme effectue un travail réel avec les E / S réelles en envoyant des requêtes HTTP à une liste d’URL et en récupérant le contenu de la page. Cependant, cela se fait de manière bloquante (synchrone).

Le programme a été modifié pour importer le merveilleux demandes module pour effectuer les requêtes HTTP réelles. En outre, la file d'attente contient maintenant une liste d'URL, plutôt que des nombres. En plus, tâche() n'incrémente plus un compteur. Au lieu, demandes récupère le contenu d'une URL extraite de la file d'attente et affiche le temps qu'il a fallu pour le faire.

le exemple_5.py le code est listé ci-dessous:

    1 importation queue
    2 importation demandes
    3 de lib.elapsed_time importation ET
    4 
    5 def tâche(Nom, work_queue):
    6     avec demandes.Session() comme session:
    sept         tandis que ne pas work_queue.vide():
    8             url = work_queue.obtenir()
    9             impression(F"Tâche Nom    obtenir l'URL: url")
dix             et = ET()
11             session.obtenir(url)
12             impression(F"Tâche Nom    temps total écoulé: et () :. 1f ")
13             rendement
14 
15 def principale():
16     "" "
17                 C'est le point d'entrée principal du programme.
18                 "" "
19     # Créer la file d'attente de 'travail'
20     work_queue = queue.Queue()
21 
22     # Mettre du "travail" dans la file d'attente
23     pour url dans [[[[
24         "http://google.com",
25         "http://yahoo.com",
26         "http://linkedin.com",
27         "http://apple.com",
28         "http://microsoft.com",
29         "http://facebook.com",
30         "http://twitter.com"
31     ]:
32         work_queue.mettre(url)
33 
34     les tâches = [[[[
35         tâche("Un", work_queue),
36         tâche("Deux", work_queue)
37     ]
38 
39     # Exécuter les tâches
40     et = ET()
41     terminé = Faux
42     tandis que ne pas terminé:
43         pour t dans les tâches:
44             essayer:
45                 suivant(t)
46             sauf StopIteration:
47                 les tâches.retirer(t)
48             si len(les tâches) == 0:
49                 terminé = Vrai
50 
51     impression(F" nTemps total écoulé: et ():. 1f ")
52 
53 si __Nom__ == "__principale__":
54     principale()

Voici ce qui se passe dans ce programme:

  • Ligne 2 importations demandes, qui fournit un moyen pratique de passer des appels HTTP.
  • Ligne 11 introduit un retard, semblable à exemple_3.py. Cependant, cette fois, il appelle session.get (url), qui renvoie le contenu de l'URL extraite de work_queue.
  • Lignes 23 à 32 mettre la liste des URL dans work_queue.

Lorsque vous exécutez ce programme, vous verrez la sortie suivante:

Première tâche pour obtenir l'URL: http://google.com
Tâche 1 Temps total écoulé: 0,3
Deuxième tâche pour obtenir l'URL: http://yahoo.com
Tâche deux temps total écoulé: 0.8
Première tâche pour obtenir l'URL: http://linkedin.com
Tâche 1 Temps total écoulé: 0.4
Obtention de l'URL de la tâche deux: http://apple.com
Tâche deux temps total écoulé: 0.3
Première tâche pour obtenir l'URL: http://microsoft.com
Tâche 1 Temps total écoulé: 0.5
Deuxième tâche pour obtenir l'URL: http://facebook.com
Tâche deux temps total écoulé: 0.5
Première tâche pour obtenir l'URL: http://twitter.com
Tâche 1 Temps total écoulé: 0.4

Temps total écoulé: 3.2

Tout comme dans les versions précédentes du programme, rendement se tourne tâche() dans un générateur. Il effectue également un changement de contexte permettant à l'autre instance de tâche de s'exécuter.

Chaque tâche obtient une URL de la file d'attente de travail, récupère le contenu de la page et indique le temps requis pour obtenir ce contenu.

Comme avant, rendement permet à vos deux tâches de s'exécuter en coopération. However, since this program is running synchronously, each session.get() call blocks the CPU until the page is retrieved. Note the total time it took to run the entire program at the end. This will be meaningful for the next example.

Asynchronous (Non-Blocking) HTTP Calls

This version of the program modifies the previous one to use Python async features. It also imports the aiohttp module, which is a library to make HTTP requests in an asynchronous fashion using asyncio.

The tasks here have been modified to remove the rendement call since the code to make the HTTP OBTENIR call is no longer blocking. It also performs a context switch back to the event loop.

le example_6.py program is listed below:

    1 importation asyncio
    2 importation aiohttp
    3 de lib.elapsed_time importation ET
    4 
    5 async def tâche(Nom, work_queue):
    6     async avec aiohttp.ClientSession() comme session:
    sept         tandis que ne pas work_queue.vide():
    8             url = attendre work_queue.obtenir()
    9             impression(f"Task name    getting URL: url")
dix             et = ET()
11             async avec session.obtenir(url) comme réponse:
12                 attendre réponse.texte()
13             impression(f"Task name    total elapsed time: et():.1f")
14 
15 async def principale():
16     """
17                 This is the main entry point for the program.
18                 """
19     # Create the queue of 'work'
20     work_queue = asyncio.Queue()
21 
22     # Put some 'work' in the queue
23     pour url dans [[[[
24         "http://google.com",
25         "http://yahoo.com",
26         "http://linkedin.com",
27         "http://apple.com",
28         "http://microsoft.com",
29         "http://facebook.com",
30         "http://twitter.com",
31     ]:
32         attendre work_queue.mettre(url)
33 
34     # Run the tasks
35     et = ET()
36     attendre asyncio.recueillir(
37         asyncio.create_task(tâche("One", work_queue)),
38         asyncio.create_task(tâche("Two", work_queue)),
39     )
40     impression(f" nTotal elapsed time: et():.1f")
41 
42 si __name__ == "__main__":
43     asyncio.courir(principale())

Here’s what’s happening in this program:

  • Line 2 imports the aiohttp library, which provides an asynchronous way to make HTTP calls.
  • Line 5 des notes task() as an asynchronous function.
  • Line 6 creates an aiohttp session context manager.
  • Line 11 creates an aiohttp response context manager. It also makes an HTTP OBTENIR call to the URL taken from work_queue.
  • Line 12 uses the response to get the text retrieved from the URL asynchronously.

When you run this program, you’ll see the following output:

Task One getting URL: http://google.com
Task Two getting URL: http://yahoo.com
Task One total elapsed time: 0.3
Task One getting URL: http://linkedin.com
Task One total elapsed time: 0.3
Task One getting URL: http://apple.com
Task One total elapsed time: 0.3
Task One getting URL: http://microsoft.com
Task Two total elapsed time: 0.9
Task Two getting URL: http://facebook.com
Task Two total elapsed time: 0.4
Task Two getting URL: http://twitter.com
Task One total elapsed time: 0.5
Task Two total elapsed time: 0.3

Total elapsed time: 1.7

Take a look at the total elapsed time, as well as the individual times to get the contents of each URL. You’ll see that the duration is about half the cumulative time of all the HTTP OBTENIR calls. This is because the HTTP OBTENIR calls are running asynchronously. In other words, you’re effectively taking better advantage of the CPU by allowing it to make multiple requests at once.

Because the CPU is so fast, this example could likely create as many tasks as there are URLs. In this case, the program’s run time would be that of the single slowest URL retrieval.

Conclusion

This article has given you the tools you need to start making asynchronous programming techniques a part of your repertoire. Using Python async features gives you programmatic control of when context switches take place. This means that many of the tougher issues you might see in threaded programming are easier to deal with.

Asynchronous programming is a powerful tool, but it isn’t useful for every kind of program. If you’re writing a program that calculates pi to the millionth decimal place, for instance, then asynchronous code won’t help you. That kind of program is CPU bound, without much IO. However, if you’re trying to implement a server or a program that performs IO (like file or network access), then using Python async features could make a huge difference.

To sum it up, you’ve learned:

  • Quoi synchronous programs sont
  • Comment asynchronous programs are different, but also powerful and manageable
  • Why you might want to write asynchronous programs
  • How to use the built-in async features in Python

You can get the code for all of the example programs used in this tutorial:

Now that you’re equipped with these powerful skills, you can take your programs to the next level!