En attendant asyncio


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.

15 thoughts on “En attendant asyncio

  • Sam Post author

    Non, il y a plein de bons outils, tous utiles pour des uses cases particuliers. Utilisez ce qui vous facilite la vie.

  • benoit

    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.

  • Sam Post author

    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.

  • Thomas

    “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é.

  • Benoit

    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 !

  • Sam Post author

    Tu parles d’IO asynchrone, mais tu mets dans un exemple des IO bloquantes dans des threads.

    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.

    Pourquoi n’as-tu pas parlé de gevent ou twisted alors ?

    Parce que ce post se concentre sur les solutions de la stdlib.

    Du coup, tu essayes de répondre à l’asynchrone, mais pas au principes d’IO.

    J’ai hate que tu m’explique ce qu’est le principe d’IO. Et ce que veut dire “répondre à l’asynchrone”.

    Mais t’as ptet jamais codé de client ou serveur un peu plus bas niveau avec poll/kpoll/select en fait ?

    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.

    Aller, je suis un mec cool, je vais te guider dans tes recherches

    C’est mignon.

    Et donc Asynchronous I/O n’est pas uniquement “asynchrone” :)

    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 :

    This module provides infrastructure for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources

    Bref, de l’IO asynchrone.

    Bonne journée.

  • Sam Post author

    @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 Post author

    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 ?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.