La programmation asynchrone arrive en force avec la version 3.4, mais celle-ci n’est pas encore en version stable. En attendant, Python 3 possède déjà de quoi faire de la programmation asynchrone, et même parallèle, avec une bien plus grande facilité qu’en Python 2.
Si vous avez oublié le principe ou l’intérêt de la programmation asynchrone, il y a un article pour ça ©.
Pour montrer l’intérêt de la chose, nous allons utiliser un bout de code pour télécharger le code HTML de pages Web.
Sans programmation asynchrone
Le code est simple et sans chichi :
# -*- coding: utf-8 -*- import datetime from urllib.request import urlopen start_time = datetime.datetime.now() URLS = ['http://sebsauvage.net/', 'http://github.com/', 'http://sametmax.com/', 'http://duckduckgo.com/', 'http://0bin.net/', 'http://bitstamp.net/'] for url in URLS: try: # j'ignore volontairement toute gestion d'erreur évoluée result = urlopen(url).read() print('%s page: %s bytes' % (url, len(result))) except Exception as e: print('%s generated an exception: %s' % (url, e)) elsapsed_time = datetime.datetime.now() - start_time print("Elapsed time: %ss" % elsapsed_time.total_seconds()) |
Ce qui nous donne:
python sans_future.py http://sebsauvage.net/ page: 9036 bytes http://github.com/ page: 12582 bytes http://sametmax.com/ generated an exception: HTTP Error 502: Bad Gateway http://duckduckgo.com/ page: 8826 bytes http://0bin.net/ page: 5551 bytes http://bitstamp.net/ page: 51996 bytes Elapsed time: 25.536095s |
Erreur 500 sur S&M… Mon script qui se fout de ma gueule en plus…
Avec programmation asynchrone
On utilise le module future, qui, comme sont nom l’indique, implémente des outils pour manipuler des “futures” en Python. Il inclut notamment un context manager pour créer, lancer et arrêter des workers automatiquement, et leur envoyer des tâches, puis récupérer les résultats de ces tâches sous forme de “futures”.
Pour rappel, une “future” est juste un objet qui représente le résultat d’une opération asynchrone (puisqu’on ne sait pas quand elle se termine). Cet objet contient des méthodes pour vérifier si le résultat est disponible à un instant t, et obtenir ce résultat si c’est le cas.
# -*- coding: utf-8 -*- import datetime import concurrent.futures from urllib.request import urlopen from concurrent.futures import ProcessPoolExecutor, as_completed start_time = datetime.datetime.now() URLS = ['http://sebsauvage.net/', 'http://github.com/', 'http://sametmax.com/', 'http://duckduckgo.com/', 'http://0bin.net/', 'http://bitstamp.net/'] def load_url(url): """ Le callback que vont appeler les workers pour télécharger le contenu d'un site. On peut appeler cela une 'tâche' """ return urlopen(url).read() # Un pool executor est un context manager qui va automatiquement créer des # processus Python séparés et répartir les tâches qu'on va lui envoyer entre # ces processus (appelés workers, ici on en utilise 5). with ProcessPoolExecutor(max_workers=5) as e: # On e.submit() envoie les tâches à l'executor qui les dispatch aux # workers. Ces derniers appelleront "load_url(url)". "e.submit()" retourne # une structure de données appelées "future", qui représente un accès au # résultat asynchrone, qu'il soit résolu ou non. futures_and_url = {e.submit(load_url, url): url for url in URLS} # "as_completed()" prend un iterable de future, et retourne un générateur # qui itère sur les futures au fur et à mesures que celles # ci sont résolues. Les premiers résultats sont donc les premiers arrivés, # donc on récupère le contenu des sites qui ont été les premiers à répondre # en premier, et non dans l'ordre des URLS. for future in as_completed(futures_and_url): # Une future est hashable, et peut donc être une clé de dictionnaire. # On s'en sert ici pour récupérer l'URL correspondant à cette future. url = futures_and_url[future] # On affiche le résultats contenu des sites si les futures le contienne. # Si elles contiennent une exception, on affiche l'exception. if future.exception() is not None: print('%s generated an exception: %s' % (url, future.exception())) else: print('%s page: %s bytes' % (url, len(future.result()))) elsapsed_time = datetime.datetime.now() - start_time print("Elapsed time: %ss" % elsapsed_time.total_seconds()) |
Et c’est quand même vachement plus rapide :
python3 avec_future.py # notez qu'on utilise Python 3 cette fois http://duckduckgo.com/ page: 8826 bytes http://sebsauvage.net/ page: 9036 bytes http://github.com/ page: 12582 bytes http://sametmax.com/ page: 50998 bytes http://0bin.net/ page: 5551 bytes http://bitstamp.net/ page: 52001 bytes Elapsed time: 3.480596s |
Même si vous retirez les commentaires, le code est encore très verbeux, ce qui explique pourquoi j’attends avec impatience asyncio qui, grâce à yield from
, va intégrer l’asynchrone de manière plus naturelle au langage.
Mais ça reste beaucoup plus simple que de créer son process à la main, créer une queue, envoyer les tâches dans la queue, s’assurer que le process est arrêté, gérer les erreurs et le clean up, etc.
Notez qu’on peut remplacer ProcessPoolExecutor
par ThreadPoolExecutor
si vous n’avez pas besoin d’un process séparé mais juste de l’IO non bloquant.
Télécharger le code de larticle : avec future / sans future.
De mon coté, j’utilise joblib. http://pythonhosted.org/joblib/ Il facilite aussi grandement la tâche. Tu as un avis Sam entre ces solutions ?
Non, il y a plein de bons outils, tous utiles pour des uses cases particuliers. Utilisez ce qui vous facilite la vie.
Woua, on parle de vous sur linuxfr.org, vous commencez à être céléèbres ;)
http://linuxfr.org/news/0-h-un-weboob
On peut envoyer des autographes à vos petits noms…
Il existe un port d’asyncio pour python2 https://pypi.python.org/pypi/trollius/
Il faut oublier les yield from mais les tasks et autres fonctionne très bien.
Hello,
Votre article parle d’asyncio mais finalement vous ne traitez pas le sujet:
“””
Notez qu’on peut remplacer ProcessPoolExecutor par ThreadPoolExecutor si vous n’avez pas besoin d’un process séparé mais juste de l’IO non bloquant.
“””
Ce n’est pas exactement le but d’asyncio d’utiliser un pool de thread ou de process, mais bien d’avoir une lib asynchrone comme par exemple https://github.com/aaugustin/websockets ou http://asyncio-redis.readthedocs.org/en/latest/
Bref, je suis persuadé que vous allez etre capables de faire un article plus complet, et je l’attend avec impatience.
concernant trollius j’ai hate de voir comment on va reussir a faire une lib compatible asyncio et trollius.
Cher Benoit, tu cumules le droit à plusieurs tampon, alors j’en ai choisis un fédérateur.
D’abord, l’article s’intitule clairement “En attendant asyncio”. Je vois pas comment on peut faire plus clair.
Ensuite, comme expliqué dans l’article pointé par le PREMIER LIEN qui est donné, l’interêt de l’asynchrone est justement de faire de l’IO non bloquant. Sinon ça n’a AUCUN interêt.
Or cet article montre exactement comment télécharger du contenu web (donc de faire de de l’IO) sans bloquer, en utilisant des pools de threads ou de process. EN ATTENDANT qu’asyncio soit de la partie avec la 3.4.
“j’attends avec impatience asyncio qui, grâce à yield from, va intégrer l’asynchrone de manière plus naturelle au langage.”
=> C’est surtout complètement différent.
asyncio repose sur l’utilisation de librairies asynchrones (select, epoll, kqueue, etc.) et non pas sur du multithreading.
Votre article ne mentionne pas cette différence fondamentale, ce qui donne l’impression que le sujet n’est pas maitrisé.
Max, Sam,
Tu parles d’IO asynchrone, mais tu mets dans un exemple des IO bloquantes dans des threads.
Pourquoi n’as-tu pas parlé de gevent ou twisted alors ?
Du coup, tu essayes de répondre à l’asynchrone, mais pas au principes d’IO.
Mais t’as ptet jamais codé de client ou serveur un peu plus bas niveau avec poll/kpoll/select en fait ?
Aller, je suis un mec cool, je vais te guider dans tes recherches:
http://man7.org/linux/man-pages/man2/socket.2.html
fais donc un “grep” sur SOCK_NONBLOCK
Merci pour tes réponses !
Ah, mais en fait t’as pas bien compris asyncio.
http://docs.python.org/3.4/library/asyncio.html
Et donc Asynchronous I/O n’est pas uniquement “asynchrone” :)
Les threads en Python ne servent qu’à ça : faire des opérations bloquantes en parallèle. Il n’y a aucun autre interêt du fait du GIL.
Parce que ce post se concentre sur les solutions de la stdlib.
J’ai hate que tu m’explique ce qu’est le principe d’IO. Et ce que veut dire “répondre à l’asynchrone”.
Mais quel rapport avec la choucroute ? C’est comme si on parlait d’outils de compilation et que tu me demandais si j’avais codé en assembleur.
C’est mignon.
Ah. Alors là effectivement je ne peux plus rien dire. Asynchronious I/O. asyncio. Ce n’est pas uniquement pour faire de l’asynchrone.
D’ailleurs, dans le lien que tu donnes, la première ligne n’est pas du tout :
Bref, de l’IO asynchrone.
Bonne journée.
@Thomas: tu encules des mouches là. L’article n’est pas un cours fondamental sur l’implémentation de l’IO aynschrone, il démontre comment faire de l’IO asynchrone facilement avec la lib standard tant qu’on a pas une solution spécialisée comme asyncio pour le faire. Bien sûr que les threads ne sont pas la même chose que le multiplexing, mais on s’en branle, ce qui nous interesse c’est que notre code n’attende pas pendant que l’IO bloque.
@ Sam en gros ton article c’est surtout “En attendant de faire des IO asynchrones, on va utiliser des threads”. Et finalement, t’as fait ça: https://medium.com/building-things-on-the-internet/40e9b2b36148 mais en moins bien.
Well done john. t’es devenu un blogueur influent :)
Oui c’est à peu près ça. Je te sens déçu mon pti Benoit. Tu espérais que je règle tes problèmes d’erection ?
“concernant trollius j’ai hate de voir comment on va reussir a faire une lib compatible asyncio et trollius.”
Bonjour, j’ai écrit une section dédiée dans la documentation:
http://trollius.readthedocs.org/#write-code-working-on-trollius-and-tulip
(Note: je suis l’auteur de Trollius.)