Python 3.5 Implémentation de la concurrence à l'aide de asyncio

Traduction du chapitre 13 Concurrence
du livre 'Expert Python Programming',
Deuxième édition
Michał Jaworski et Tarek Ziadé, 2016

Programmation asynchrone


Ces dernières années, la programmation asynchrone a gagné en popularité. Python 3.5 a enfin obtenu quelques fonctions de syntaxe qui renforcent les concepts de solutions asynchrones. Mais cela ne signifie pas que la programmation asynchrone n'est devenue possible que depuis Python 3.5. De nombreuses bibliothèques et frameworks ont été fournis beaucoup plus tôt, et la plupart d'entre eux provenaient d'anciennes versions de Python 2. Il existe même une implémentation alternative de Python appelée Stackless (voir le chapitre 1, «Le statut actuel de Python»), qui se concentre sur cette approche de programmation unique. Pour certaines solutions, telles que Twisted, Tornado ou Eventlet , des communautés actives existent toujours et valent vraiment la peine d'être connues. Dans tous les cas, à partir de Python 3.5, la programmation asynchrone est devenue plus facile que jamais. Ainsi, il est prévu que ses fonctions asynchrones intégrées remplaceront la plupart des anciens outils, ou que les projets externes se transformeront progressivement en une sorte de cadres de haut niveau basés sur Python intégré.

Lorsque vous essayez d'expliquer ce qu'est la programmation asynchrone, il est plus facile de considérer cette approche comme quelque chose de similaire aux threads, mais sans planificateur système. Cela signifie qu'un programme asynchrone peut traiter des tâches en même temps, mais son contexte est changé en interne et non par le planificateur système.

Mais, bien sûr, nous n'utilisons pas de threads pour le traitement parallèle des tâches dans un programme asynchrone. La plupart des solutions utilisent des concepts différents et, selon l'implémentation, sont appelées différemment. Voici quelques exemples de noms utilisés pour décrire ces objets de programme parallèles:

  • Fils verts - Fils verts (projets greenlet, gevent ou eventlet)
  • Coroutines - coroutines (programmation asynchrone pure en Python 3.5)
  • Tasklets (Stackless Python) Ce sont essentiellement les mêmes concepts, mais souvent implémentés de manières légèrement différentes.

Pour des raisons évidentes, dans cette section, nous nous concentrerons uniquement sur les coroutines initialement prises en charge par Python, à partir de la version 3.5.

Multitâche collaboratif et E / S asynchrones


Le multitâche collaboratif est au cœur de la programmation asynchrone. En ce sens, le multitâche dans le système d'exploitation n'est pas requis pour initier un changement de contexte (vers un autre processus ou thread), mais à la place, chaque processus libère volontairement le contrôle lorsqu'il est en mode veille pour assurer l'exécution simultanée de plusieurs programmes. C'est pourquoi il est appelé collaboratif. Tous les processus doivent travailler ensemble pour garantir le succès du multitâche.

Le modèle multitâche était parfois utilisé dans les systèmes d'exploitation, mais il ne peut désormais plus être trouvé comme une solution au niveau du système. En effet, il existe un risque qu'un service mal conçu puisse facilement perturber la stabilité de l'ensemble du système. La planification des threads et des processus à l'aide de commutateurs de contexte contrôlés directement par le système d'exploitation est actuellement l'approche dominante pour la concurrence au niveau du système. Mais le multitâche collaboratif est toujours un excellent outil de concurrence au niveau de l'application.

En parlant de multitâche conjoint au niveau de l'application, nous ne traitons pas de threads ou de processus qui doivent libérer le contrôle, car toute l'exécution est contenue dans un processus et un thread. Au lieu de cela, nous avons plusieurs tâches (coroutines, tasklets et fils verts) qui transfèrent le contrôle à une seule fonction qui contrôle la coordination des tâches. Cette fonction est généralement une sorte de boucle d'événement.

Pour éviter toute confusion (en raison de la terminologie Python), nous allons maintenant appeler ces tâches parallèles coroutines. Le problème le plus important dans le multitâche collaboratif est de savoir quand transférer le contrôle. Dans la plupart des applications asynchrones, le contrôle est transmis au planificateur ou à la boucle d'événements pendant les opérations d'E / S. Que le programme lise les données du système de fichiers ou communique via un socket, une telle opération d'E / S est toujours associée à un certain temps d'attente lorsque le processus devient inactif. La latence dépend d'une ressource externe, c'est donc une bonne occasion de libérer le contrôle afin que d'autres coroutines puissent faire leur travail, jusqu'à ce qu'elles doivent également attendre que cette approche soit quelque peu similaire dans son comportement à la façon dont le multithreading est implémenté en Python. Nous savons que le GIL sérialise les threads Python, mais il est également libéré à chaque opération d'E / S. La principale différence est que les threads en Python sont implémentés en tant que threads au niveau du système, de sorte que le système d'exploitation peut décharger le thread en cours d'exécution à tout moment et transférer le contrôle à un autre.

En programmation asynchrone, les tâches ne sont jamais interrompues par la boucle d'événement principale. C'est pourquoi ce style multitâche est également appelé multitâche non prioritaire.

Bien sûr, chaque application Python s'exécute sur un système d'exploitation où d'autres processus se disputent les ressources. Cela signifie que le système d'exploitation a toujours le droit de décharger l'ensemble du processus et de transférer le contrôle à un autre. Mais lorsque notre application asynchrone redémarre, elle reprend là où elle a été interrompue lorsque le planificateur système est intervenu. C'est pourquoi les coroutines dans ce contexte sont considérées comme non encombrantes.

Python asynchrone et attend les mots clés


Les mots clés asynchrones et attendent sont les principaux éléments constitutifs de la programmation Python asynchrone.

Le mot-clé async utilisé avant l'instruction def définit une nouvelle coroutine. Une fonction coroutine peut être suspendue et reprise dans des circonstances strictement définies. Sa syntaxe et son comportement sont très similaires à ceux des générateurs (voir Chapitre 2, «Recommandations de syntaxe», sous le niveau de la classe). En fait, les générateurs devraient être utilisés dans les anciennes versions de Python pour implémenter les coroutines. Voici un exemple de déclaration d'une fonction qui utilise le mot clé async :

async def async_hello(): print("hello, world!") 

Les fonctions définies à l'aide du mot - clé async sont spéciales. Lorsqu'ils sont appelés, ils n'exécutent pas de code à l'intérieur, mais renvoient à la place un objet coroutine:

 >>>> async def async_hello(): ... print("hello, world!") ... >>> async_hello() <coroutine object async_hello at 0x1014129e8> 

L'objet coroutine ne fait rien tant que son exécution n'est pas planifiée dans la boucle d'événement. Le module asyncio est disponible pour fournir une implémentation de base de la boucle d'événements, ainsi que de nombreux autres utilitaires asynchrones:

 >>> import asyncio >>> async def async_hello(): ... print("hello, world!") ... >>> loop = asyncio.get_event_loop() >>> loop.run_until_complete(async_hello()) hello, world! >>> loop.close() 

Naturellement, en ne créant qu'une seule coroutine simple, dans notre programme nous n'implémentons pas le parallélisme. Pour voir quelque chose de vraiment parallèle, nous devons créer plus de tâches qui seront effectuées par une boucle d'événement.

De nouvelles tâches peuvent être ajoutées à la boucle en appelant la méthode loop.create_task () ou en fournissant un autre objet pour attendre l'utilisation de la fonction asyncio.wait () . Nous allons utiliser cette dernière approche et essayer d'imprimer de manière asynchrone une séquence de nombres générée à l'aide de la fonction range () :

 import asyncio async def print_number(number): print(number) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete( asyncio.wait([ print_number(number) for number in range(10) ]) ) loop.close() 

La fonction asyncio.wait () accepte une liste d'objets coroutine et retourne immédiatement. Le résultat est un générateur qui produit des objets représentant des résultats futurs (futurs). Comme son nom l'indique, il est utilisé pour attendre que toutes les coroutines fournies soient terminées. La raison pour laquelle il retourne un générateur au lieu d'un objet coroutine est qu'il est rétrocompatible avec les versions précédentes de Python, ce qui sera expliqué plus loin. Le résultat de l'exécution de ce script peut être le suivant:

 $ python asyncprint.py 0 7 8 3 9 4 1 5 2 6 

Comme nous pouvons le voir, les chiffres ne sont pas imprimés dans l'ordre dans lequel nous avons créé nos coroutines. Mais c'est exactement ce que nous voulions réaliser.

Le deuxième mot clé important ajouté dans Python 3.5 est en attente . Il est utilisé pour attendre les résultats d'une coroutine ou d'un événement futur (expliqué plus loin) et libérer le contrôle de l'exécution dans la boucle d'événement. Pour mieux comprendre comment cela fonctionne, nous devons considérer un exemple de code plus complexe.

Supposons que nous voulons créer deux coroutines qui effectueront des tâches simples en boucle:

  • Attendez un nombre aléatoire de secondes
  • Imprimez le texte fourni comme argument et le temps d'attente. Commençons par une implémentation simple qui a quelques problèmes de concurrence que nous essaierons d'améliorer plus tard avec l'utilisation supplémentaire de wait:

     import time import random import asyncio async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 time.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) async def main(): await asyncio.wait([waiter("foo"), waiter("bar")]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() 

Lorsqu'il est exécuté dans le terminal (en utilisant la commande time pour mesurer le temps), vous pouvez voir:

 $ time python corowait.py bar waited 0.25 seconds bar waited 0.25 seconds bar waited 0.5 seconds bar waited 0.5 seconds foo waited 0.75 seconds foo waited 0.75 seconds foo waited 0.25 seconds foo waited 0.25 seconds real 0m3.734s user 0m0.153s sys 0m0.028s 


Comme nous pouvons le voir, les deux coroutines ont terminé leur exécution, mais pas de manière asynchrone. La raison en est qu'ils utilisent tous les deux la fonction time.sleep () , qui verrouille mais ne libère pas le contrôle dans la boucle d'événement. Cela fonctionnera mieux dans une installation multi-thread, mais nous ne voulons pas utiliser de flux pour le moment. Alors, comment pouvons-nous résoudre ce problème?

La réponse consiste à utiliser asyncio.sleep () , qui est une version asynchrone de time.sleep (), et à attendre le résultat à l'aide du mot clé wait. Nous avons déjà utilisé cette instruction dans la première version de main () , mais c'était uniquement pour améliorer la clarté du code. Cela n'a clairement pas rendu notre mise en œuvre plus parallèle. Regardons une version améliorée de la coroutine waiter () qui utilise attendent asyncio.sleep ():

 async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 await asyncio.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) 


En exécutant le script mis à jour, nous verrons comment la sortie de deux fonctions alternent entre elles:

 $ time python corowait_improved.py bar waited 0.25 seconds foo waited 0.25 seconds bar waited 0.25 seconds foo waited 0.5 seconds foo waited 0.25 seconds bar waited 0.75 seconds foo waited 0.25 seconds bar waited 0.5 seconds real 0m1.953s user 0m0.149s sys 0m0.026s 


Un avantage supplémentaire de cette simple amélioration est que le code s'exécute plus rapidement. Le temps d'exécution total était inférieur à la somme de tous les temps de sommeil, car les coroutines ont pris le contrôle un par un.

Asyncio dans les versions précédentes de Python


Le module asyncio est apparu en Python 3.4. C'est donc la seule version de Python qui prend en charge sérieusement la programmation asynchrone avant Python 3.5. Malheureusement, il semble que ces deux versions ultérieures soient suffisantes pour présenter des problèmes de compatibilité.

Quoi qu'il en soit, le noyau de programmation asynchrone en Python a été introduit plus tôt que les éléments de syntaxe qui prennent en charge ce modèle. Mieux vaut tard que jamais, mais cela a créé une situation où il existe deux syntaxes pour travailler avec des coroutines.

À partir de Python 3.5, vous pouvez utiliser async et attendre :

 async def main (): await asyncio.sleep(0) 


Cependant, dans Python 3.4, vous devrez en plus appliquer le décorateur asyncio.coroutine et donner le texte dans le texte coroutine:

 @asyncio.couroutine def main(): yield from asyncio.sleep(0) 


Un autre fait utile est que le rendement de l'instruction a été introduit dans Python 3.3, et PyPI a un backport asynchrone. Cela signifie que vous pouvez également utiliser cette implémentation du multitâche collaboratif avec Python 3.3.

Un exemple pratique de programmation asynchrone


Comme mentionné à plusieurs reprises dans ce chapitre, la programmation asynchrone est un excellent outil pour gérer les E / S. Il est temps de créer quelque chose de plus pratique que d'imprimer des séquences ou une attente asynchrone.

Afin d'assurer la cohérence, nous essaierons de résoudre le même problème que nous avons résolu à l'aide du multithreading et du multiprocessing. Par conséquent, nous essaierons d'extraire de manière asynchrone certaines données de ressources externes via une connexion réseau. Ce serait formidable si nous pouvions utiliser le même paquet python-gmaps que dans les sections précédentes. Malheureusement, nous ne pouvons pas.

Le créateur de python-gmaps était un peu paresseux et ne prenait que le nom. Pour simplifier le développement, il a choisi le package de requête comme bibliothèque client HTTP. Malheureusement, les demandes ne prennent pas en charge les E / S asynchrones avec async et attendent . Il existe d'autres projets qui visent à fournir un certain parallélisme pour le projet de requête, mais ils s'appuient sur Gevent ( grequests , voir https://github.com/ kennethreitz / grequests ) ou exécutent un pool de threads / processus (query-futures voir github.com/ross/requests-futures ). Aucun d'eux ne résout notre problème.

Avant de me reprocher d'avoir réprimandé un développeur open source innocent, calmez-vous. La personne derrière le paquet python-gmaps est moi. Un mauvais choix de dépendances est l'un des problèmes de ce projet. J'aime juste me critiquer publiquement de temps en temps. Ce sera une leçon amère pour moi, car python-gmaps dans sa dernière version (0.3.1 au moment de la rédaction de ce livre) ne peut pas être facilement intégré avec les E / S asynchrones de Python. Dans tous les cas, cela peut changer à l'avenir, donc rien n'est perdu.
Connaissant les limites de la bibliothèque, qui était si facile à utiliser dans les exemples précédents, nous devons créer quelque chose qui comble cette lacune. Google MapsAPI est vraiment facile à utiliser, nous allons donc créer un utilitaire asynchrone à titre d'illustration. Il manque toujours à la bibliothèque standard de Python 3.5 une bibliothèque qui puisse exécuter des requêtes HTTP asynchrones aussi facilement que d'appeler urllib.urlopen () . Nous ne voulons certainement pas créer un support de protocole complet à partir de zéro, nous allons donc utiliser un peu d'aide du package aiohttp disponible dans PyPI. Il s'agit d'une bibliothèque très prometteuse qui ajoute des implémentations client et serveur pour HTTP asynchrone. Voici un petit module intégré à aiohttp qui crée une fonction d'assistance geocode () qui exécute des demandes de géocodage au service API Google Maps:

 import aiohttp session = aiohttp.ClientSession() async def geocode(place): params = { 'sensor': 'false', 'address': place } async with session.get( 'https://maps.googleapis.com/maps/api/geocode/json', params=params ) as response: result = await response.json() return result['results'] 


Supposons que ce code soit stocké dans un module nommé asyncgmaps , que nous utiliserons plus tard. Nous sommes maintenant prêts à réécrire l'exemple utilisé dans la discussion sur le multithreading et le multiprocessing. Auparavant, nous avions l'habitude de séparer l'ensemble de l'opération en deux étapes distinctes:

  1. Répondez à toutes les demandes au service externe en parallèle à l'aide de la fonction fetch_place () .
  2. Affichez tous les résultats dans une boucle en utilisant la fonction present_result () .

Mais comme le multitâche collaboratif est complètement différent de l'utilisation de plusieurs processus ou threads, nous pouvons légèrement changer notre approche. La plupart des problèmes soulevés dans Utilisation d'un seul thread par article ne nous concernent plus.
Les coroutines ne sont pas préemptives, nous pouvons donc facilement afficher les résultats immédiatement après avoir reçu des réponses HTTP. Cela simplifiera notre code et le rendra plus compréhensible:

 import asyncio # note: local module introduced earlier from asyncgmaps import geocode, session PLACES = ( 'Reykjavik', 'Vien', 'Zadar', 'Venice', 'Wrocław', 'Bolognia', 'Berlin', 'Słubice', 'New York', 'Dehli', ) async def fetch_place(place): return (await geocode(place))[0] async def present_result(result): geocoded = await result print("{:>25s}, {:6.2f}, {:6.2f}".format( geocoded['formatted_address'], geocoded['geometry']['location']['lat'], geocoded['geometry']['location']['lng'], )) async def main(): await asyncio.wait([ present_result(fetch_place(place)) for place in PLACES ]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) # aiohttp will raise issue about unclosed # ClientSession so we perform cleanup manually loop.run_until_complete(session.close()) loop.close() 


La programmation asynchrone est idéale pour les développeurs d'arrière-plan intéressés par la création d'applications évolutives. En pratique, c'est l'un des outils les plus importants pour créer des serveurs hautement compétitifs.

Mais la réalité est triste. De nombreux packages courants traitant des problèmes d'E / S ne sont pas destinés à être utilisés avec du code asynchrone. Les principales raisons en sont:

  • Implémentation encore faible de Python 3 et de certaines de ses fonctionnalités avancées
  • Faible compréhension des différents concepts de concurrence chez les débutants pour apprendre Python

Cela signifie que très souvent, la migration des applications et packages multithreads synchrones existants est soit impossible (en raison de restrictions architecturales), soit trop coûteuse. De nombreux projets pourraient grandement bénéficier de la mise en œuvre du style multitâche asynchrone, mais seuls quelques-uns finiront par le faire. Cela signifie qu'en ce moment, vous aurez beaucoup de difficultés à créer des applications asynchrones dès le début. Dans la plupart des cas, cela ressemblera au problème mentionné dans la section «Exemple pratique de programmation asynchrone» - interfaces incompatibles et blocage non synchrone des opérations d'E / S. Bien sûr, vous pouvez parfois renoncer à attendre lorsque vous rencontrez une telle incompatibilité et obtenir simplement les ressources nécessaires de manière synchrone. Mais cela empêchera l'autre coroutine d'exécuter son code pendant que vous attendez les résultats. Techniquement, cela fonctionne, mais détruit également tous les avantages de la programmation asynchrone. Ainsi, au final, la combinaison d'E / S asynchrones avec des E / S synchrones n'est pas une option. C'est un jeu tout ou rien.

Un autre problème est la longueur des opérations liées au processeur. Lorsque vous effectuez une opération d'E / S, il n'y a aucun problème à libérer le contrôle d'une coroutine. Lors de l'écriture / lecture à partir d'un système de fichiers ou d'un socket, vous finirez par attendre, donc un appel utilisant wait est le meilleur que vous puissiez faire. Mais que faire si vous avez besoin de calculer quelque chose et que vous savez que cela prendra un certain temps? Bien sûr, vous pouvez diviser le problème en parties et annuler le contrôle chaque fois que vous avancez un peu le travail. Mais bientôt vous constaterez que ce n'est pas un très bon modèle. Une telle chose peut rendre le code désordonné et ne garantit pas non plus de bons résultats.

La liaison temporelle devrait être la responsabilité de l'interprète ou du système d'exploitation.

Combinaison de code asynchrone avec des contrats à terme asynchrones


Que faire si vous avez du code qui exécute de longues E / S synchrones que vous ne pouvez pas ou ne voulez pas réécrire. Ou que faire lorsque vous devez effectuer des opérations de processeur lourdes dans une application conçue principalement pour les E / S asynchrones? Eh bien ... vous devez trouver une solution de contournement. Et j'entends par là le multithreading ou le multiprocessing.

Cela peut ne pas sembler très bon, mais parfois la meilleure solution peut être ce dont nous avons essayé de nous éloigner. Le traitement parallèle des tâches gourmandes en ressources en Python est toujours mieux exécuté grâce au multitraitement. Et le multithreading peut gérer les opérations d'E / S tout aussi bien (rapidement et sans beaucoup de ressources), comme asynchrone et en attente s'il est correctement configuré et géré avec soin.

Donc, parfois, lorsque vous ne savez pas quoi faire lorsque quelque chose ne correspond tout simplement pas à votre application asynchrone, utilisez un morceau de code qui le place sur un thread ou un processus distinct. Vous pouvez prétendre qu'il s'agit d'une coroutine, libérer le contrôle de la boucle d'événements et finalement traiter les résultats lorsqu'ils sont prêts.

Heureusement pour nous, la bibliothèque standard Python fournit le module concurrent.futures , qui est également intégré au module asyncio . Ensemble, ces deux modules vous permettent de planifier des fonctions de blocage qui sont exécutées dans des threads ou des processus supplémentaires, comme s'il s'agissait de coroutines asynchrones non bloquantes.

Exécuteurs testamentaires et contrats à terme


Avant de voir comment incorporer des threads ou des processus dans une boucle d'événement asynchrone, nous examinons de plus près le module concurrent.futures , qui deviendra plus tard le composant principal de notre soi-disant solution de contournement.

Les classes les plus importantes du module concurrent.futures sont Executor et Future .

Executor est un pool de ressources qui peut traiter des éléments de travail en parallèle. Il peut sembler très similaire dans son objectif aux classes du module multiprocesseur - Pool et dummy.Pool - mais il a une interface et une sémantique complètement différentes. Il s'agit d'une classe de base qui n'est pas destinée à être implémentée et possède deux implémentations spécifiques:

  • ThreadPoolExecutor : qui représente un pool de threads
  • ProcessPoolExecutor : qui représente un pool de processus

Chaque exécuteur testamentaire présente trois méthodes:

  • submit (fn, * args, ** kwargs) : planifie l'exécution de la fonction fn dans le pool de ressources et retourne un objet Future représentant l'exécution de l'objet appelé
  • map (func, * iterables, timeout = None, chunksize = 1) : la fonction func est exécutée sur l'itération de manière similaire au multiprocessing. Méthode Pool.map ()
  • shutdown (wait = True) : cela arrête l' Executor et libère toutes ses ressources.

La méthode la plus intéressante est submit () en raison de l'objet Future qu'elle renvoie. Il représente l'exécution asynchrone de l'appelé et ne représente qu'indirectement son résultat. Pour obtenir la valeur de retour réelle de l'objet appelé distribué, vous devez appeler la méthode Future.result () . Et si l'objet appelé est déjà terminé, la méthode result () ne le bloquera pas et retournera simplement la sortie de la fonction. Si ce n'est pas le cas, il le bloquera jusqu'à ce que le résultat soit prêt. Considérez-le comme une promesse de résultat (c'est en fait le même concept qu'une promesse en JavaScript). Vous n'avez pas besoin de le décompresser immédiatement après l'avoir reçu (en utilisant la méthode result () ), mais si vous essayez de le faire, il est garanti de retourner éventuellement quelque chose:

 >>> def loudy_return(): ... print("processing") ... return 42 ... >>> from concurrent.futures import ThreadPoolExecutor >>> with ThreadPoolExecutor(1) as executor: ... future = executor.submit(loudy_return) ... processing >>> future <Future at 0x33cbf98 state=finished returned int> >>> future.result() 42 


Si vous souhaitez utiliser la méthode Executor.map () , son utilisation n'est pas différente de la méthode Pool.map () de la classe Pool du module multiprocesseur:

 def main(): with ThreadPoolExecutor(POOL_SIZE) as pool: results = pool.map(fetch_place, PLACES) for result in results: present_result(result) 


Utilisation de l' exécuteur dans une boucle d' événement


Les instances de la classe Future renvoyées par la méthode Executor.submit () sont conceptuellement très proches des coroutines utilisées dans la programmation asynchrone. C'est pourquoi nous pouvons utiliser des artistes pour créer un hybride entre le multitâche collaboratif et le multitraitement ou le multithreading.

Le cœur de cette solution de contournement est la méthode BaseEventLoop.run_in_executor (executor, func, * args) de la classe de boucle d' événements . Cela vous permet de planifier l'exécution de la fonction func dans un processus ou un pool de threads représenté par l'argument exécuteur. La chose la plus importante à propos de cette méthode est qu'elle renvoie le nouvel objet attendu (l'objet auquel on peut s'attendre à l'aide de l'opérateur wait). Ainsi, grâce à cela, vous pouvez effectuer une fonction de blocage qui n'est pas une coroutine exactement comme une coroutine, et elle ne se bloquera pas, quel que soit le temps nécessaire pour terminer. Il arrêtera uniquement la fonction qui attend les résultats d'un tel appel, mais tout le cycle d'événements se poursuivra.

Et un fait utile est que vous n'avez même pas besoin de créer votre propre instance d'exécuteur testamentaire. Si vous passez None comme argument à executor , la classe ThreadPoolExecutor sera utilisée avec le nombre de threads par défaut (pour Python 3.5, c'est le nombre de processeurs multiplié par 5).

Supposons donc que nous ne voulions pas réécrire la partie problématique du paquet python-gmaps qui causait nos maux de tête. Nous pouvons facilement reporter un appel de blocage à un thread séparé en appelant loop.run_in_executor () , tout en laissant la fonction fetch_place () comme coroutine attendue:

 async def fetch_place(place): coro = loop.run_in_executor(None, api.geocode, place) result = await coro return result[0] 


Une telle solution est pire que d'avoir une bibliothèque entièrement asynchrone pour faire le travail, mais vous savez qu'au moins quelque chose vaut mieux que rien.

Après avoir expliqué ce qu'est réellement la concurrence, nous avons pris des mesures et analysé l'un des problèmes parallèles typiques en utilisant le multithreading. Après avoir identifié les principales lacunes de notre code et les avoir corrigées, nous nous sommes tournés vers le multitraitement pour voir comment cela fonctionnerait dans notre cas.

Après cela, nous avons constaté qu'avec un module multiprocesseur, l'utilisation de plusieurs processus est beaucoup plus facile que les threads de base avec le multithreading. Mais seulement après cela, nous avons réalisé que nous pouvons utiliser la même API avec des threads, grâce à multiprocessing.dummy. Ainsi, le choix entre le multitraitement et le multithread dépend désormais uniquement de la solution la mieux adaptée au problème et non de la solution qui a la meilleure interface.

En parlant d'adapter le problème, nous avons finalement essayé la programmation asynchrone, qui devrait être la meilleure solution pour les applications liées aux E / S, seulement pour comprendre que nous ne pouvons pas complètement oublier les threads et les processus. Nous avons donc fait un cercle, là où nous avons commencé!

Et cela nous amène à la conclusion finale de ce chapitre. Il n'y a pas de solution qui convienne à tout le monde. Il existe plusieurs approches que vous pouvez préférer ou aimer davantage. Certaines approches conviennent mieux à cet ensemble de problèmes, mais vous devez toutes les connaître pour réussir. Dans des scénarios réalistes, vous pouvez utiliser tout l'arsenal d'outils et de styles de parallélisme dans une seule application, ce qui n'est pas rare.

La conclusion précédente est une excellente introduction au sujet du chapitre suivant, Chapitre 14 «Modèles de conception utiles». Puisqu'il n'y a pas de modèle unique qui résoudra tous vos problèmes. Vous devez en savoir autant que possible, car en fin de compte, vous les utiliserez tous les jours.

Source: https://habr.com/ru/post/fr484446/


All Articles