Sam & Max » future http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Deferred, Future et Promise : le pourquoi, le comment, et quand est-ce qu’on mange 18 http://sametmax.com/deferred-future-et-promise-le-pourquoi-le-comment-et-quand-est-ce-quon-mange/ http://sametmax.com/deferred-future-et-promise-le-pourquoi-le-comment-et-quand-est-ce-quon-mange/#comments Wed, 04 Jun 2014 13:19:22 +0000 http://sametmax.com/?p=10418 Si vous avez plongé dans le monde de la programmation asynchrone non bloquante, vous avez du vous heurter aux callbacks. Si ce n’est pas le cas, aller lire l’article, et faites vos armes sur jQuery, je vais m’en servir en exemple.

Signalement de rigueur que l’article est long :

Un callback, ça va.

Deux callbacks, pour un seul appel, ça commence à être chiant, mais c’est compréhensible.

Quand les callbacks appellent eux aussi des callbacks, ça donne des codes imbitables :

$(function(){
  $.post('/auth/token', function(token){
    saveToken(token);
    $.get('/sessions/last', function(session){
      if (session.device != currentDevice){
        $.get('/session/ ' + session.id + '/context', function(context){
          loadContext(function(){
            startApp(function(){
              initUi()
            })
          })}
        )}
      else {
        startApp(function(){
          initUi()
        })
      }}
    )
  })
});

Il y a pire que de lire ce code : le modifier ! Retirez un bloc, pour voir. Oh, et histoire de vous faire partager l’expérience complète, j’ai volontairement déplacé l’indentation d’une parenthèse et de deux brackets.

Or les codes asynchrones ont besoin de callback afin d’enchainer certaines opérations dans le bon ordre, sinon on ne peut pas récupérer le résultat d’une fonction et l’utiliser dans une autre, puisqu’on ne sait pas quand l’opération se termine.

Dans notre exemple, $.post et $.get font des requêtes POST et GET, et comme on ne sait pas quand le serveur va répondre, il faut mettre un callback pour gérer la réponse quand elle arrive. C’est plus performant que de bloquer jusqu’à ce que la première requête soit terminée car pendant ce temps, notre programme peut faire autre chose. Mais c’est aussi super relou à écrire et comprendre.

Entrent en jeu les promesses (promises). Ou les deferred. Ou les futures.

Typiquement, on retrouve des deferreds dans Twisted, des promises pour l’AJAX avec jQuery, des futures pour asyncio… Mais il y en a un peu partout de nos jours, et une lib peut utiliser plusieurs de ces concepts.

En fait c’est la même chose, un nom différent donné au même concept, par des gens qui l’ont réinventé dans leur coin. Les puristes vous diront qu’il y a des différences dans l’implémentation, ou alors que la promesse est l’interface tandis que le deferred est l’objet retourné, bla, bla, bla.

Fuck it, on va considérer que c’est tout pareil.

Les promesses sont une des manières de rendre un code asynchrone plus facile à gérer. On dit : ce groupe de fonctions doit s’exécuter dans un ordre car elles sont dépendantes les unes des autres.

Il y a d’autres moyens de gérer le problème de l’asynchrone: des événements, des queues, etc. L’avantage des promesses c’est que c’est assez simple, et ça marche là où on utilisait des callbacks avant, donc on a pu les rajouter aux libs qui étaient blindées de callbacks.

Le principe

La promesse est un moyen de dire que certaines fonctions, bien que non bloquantes et asynchrones, sont liées entre elles, et doivent s’exécuter les unes à la suite des autres. Cela permet de donner un ordre d’exécution à un groupe de fonctions, et surtout, que chaque fonction puisse accéder au résultat de la fonction précédente. Tout ceci sans bloquer le reste du système asynchrone.

En résumé, cela donne un gout de programmation synchrone, à quelque chose qui ne l’est pas.

Cela se passe ainsi :

  • La fonction asynchrone retourne un objet immédiatement : la promesse.
  • On ne passe pas de callback à la fonction. On rajoute un callback à la promesse.
  • Le callback prend en paramètre le résultat de la fonction asynchrone.
  • Le callback retourne le résultat de son traitement.
  • On peut rajouter autant de callbacks qu’on veut à la promesse, chacun devant accepter le résultat du callback précédent et retourner son propre résultat.
  • Si un des callbacks retourne une promesse, elle est fusionnée avec la promesse initiale, et c’est son résultat que le prochain callback va récupérer

Voilà un exemple :

// $.get est asynchrone. On a pas le résultat tout de suite, mais en attendant
// on a une promesse tout de suite.
var $promesse = $.get('/truc/machin');
 
// premier callback. Il sera appelé quand $.get aura récupéré son
// résultat
$promesse.then(function(resultat){
  // faire un truc avec le résultat
  // puis on retourne le nouveau résultat
  return nouveau_resultat;
});
 
// deuxième callback. Il sera appelé quand le premier callback
// aura retourné son résultat.
$promesse.then(function(nouveau_resultat){
  // faire un truc
});

Notez bien que c’est TRES différent de ça (en Python):

resultat = request.get('/truc/marchin')
 
def function(resultat):
  # faire un truc
  return nouveau_resultat
nouveau_resultat = function(resultat)
 
def autre_function(nouveau_resultat):
  # faire un truc
autre_function(nouveau_resultat)

En Python, le code est bloquant par défaut. Ça va marcher, mais pendant que le code attend la réponse du serveur, votre ordinateur est en pause et ne travaille pas.

Un plus beau code

On se retrouve avec un code asynchrone, mais qui s’exécute dans l’ordre de lecture. Et comme on peut chainer les then() et donc ne pas réécrire $promesse à chaque fois, on obtient quelque chose de beaucoup plus lisible :

$.get('/truc/machin')
.then(function(resultat){
  // faire un truc
  return nouveau_resultat;
})
.then(function(nouveau_resultat){
  // faire un truc
});

Si on reprend notre premier exemple, ça donne ça :

$(function(){
 
// create new token
$.post('/auth/token')
 
// then save token and get last session
.then(function(token){
  saveToken(token);
  return $.get('/sessions/last');
})
 
// then init session
.then(function(session){
  if (session.device != currentDevice){
 
    $.get('/session/ ' + session.id + '/context')
    .then(function(context){
      loadContext(function(){
        startApp(function(){
          initUi()
        })
      })
    })
 
  }
  else {
    startApp(function(){
      initUi()
    })
  }}
})
 
});

Tout ça s’exécute de manière non bloquante (d’autres fonctions ailleurs dans le programme peuvent s’exécuter pendant qu’on attend la réponse du serveur), mais dans l’ordre de lecture, donc on comprend bien ce qui se passe. Si on veut retirer un bloc, c’est beaucoup plus facile.

Comment ça marche à l’intérieur ?

Histoire d’avoir une idée de comment une promise marche, on va faire une implémentation, simpliste et naïve, mais compréhensible, d’une promesse en Python. Pour rendre l’API un peu sympa,je vais utiliser les décorateurs.

class Promise:
 
    # La promesse contient une liste de callbacks, donc une liste de fonctions.
    # Pas le résultat des fonctions, mais bien les fonctions elles mêmes,
    # puisque les fonctions sont manipulables en Python.
    def __init__(self):
        self.callbacks = []
 
    # Point d'entrée pour ajouter un callback à la promesse
    def then(self, callback):
        self.callbacks.append(callback)
 
    # Cette méthode est celle qui sera appelée par le code asynchrone
    # quand il reçoit son résultat.
    def resolve(self, resultat):
 
        # Ici, on obtient le résultat du code asycnhrone, donc on boucle
        # sur les callbacks pour les appeler
        while self.callbacks:
            # On retire le premier callback de la liste, et on l'appelle
            # avec le résultat
            resultat = self.callbacks.pop(0)(resultat)
 
            # Si le resultat est une promesse, on dit à cette nouvelle promesse
            # de nous rappeler quand elle a reçu ses résultats à elle avant
            # d'aller le reste de nos callbacks à nous : on fusionne les deux
            # promesses :
            # Promesse 1
            #  - callback1
            #  - callback2
            #  - Promesse 2
            #      * callback 1
            #      * callback 2
            #  - callback 3
            if isinstance(resultat, Promise):
                resultat.then(self.resolve)
                break

Maintenant, créons un code asynchrone:

from threading import Timer
 
def func1(v1):
    # On dit complètement artificiellement d'afficher le résultat
    # de la fonction dans 3 secondes, sans bloquer histoire d'avoir
    # un peu de nonbloquitude dans notre code et justifier l'asynchrone.
    def callback1():
        print(v1)
    t = Timer(3, callback1)
    t.start()
 
def func2(v2):
    # Le même, mais pour 2 secondes
    def callback2():
        print(v2)
    t = Timer(2, callback2)
    t.start()
 
# Deux fonctions normales
def func3(v3):
    print(v3)
 
def func4(v4):
    print(v4)
 
# Et si on les enchaines...
print('Je commence')
func1(1)
print('Juste après')
func2(2)
func3(3)
func4(4)
 
# ... le résultat est bien désordonné :
 
## Je commence
## Juste après
## 3
## 4
## 2
## 1

Parfois c’est ce que l’on veut, que les choses s’exécutent dans le désordre, sans bloquer.

Mais quand on a des fonctions qui dépendent les unes des autres, au milieu d’un code asynchrone, on veut qu’elles se transmettent le résultat les unes aux autres au bon moment. Pour cela, utilisons notre promesse :

from threading import Timer
 
 
# La mise en place de promesses suppose que le code 
# écrit en fasse explicitement usage. Notre code est
# définitivement lié à cette manière de faire.
 
def func1(v1):
    # Notre fonction doit créer la promesse et la retourner
    p = Promise()
    def callback1():
        print(v1)
        # Dans le callback, elle doit dire quand la promesse est tenue
        p.resolve(v1)
    t = Timer(3, callback1)
    t.start()
    return p
 
# On lance la première fonction.
print('Je commence')
promise = func1(1)
print('Juste après')
 
# On ajoute des callbacks à notre promesse.
 
@promise.then
def func2(v2):
    p = Promise()
    def callback2():
        # Pour justifier l’enchainement des fonctions, on fait en sorte que
        # chaque fonction attend le résultat de la précédente, et
        # l'incrémente de 1.
        print(v2 + 1)
        p.resolve(v2 + 1)
    t = Timer(2, callback2)
    t.start()
    # Ce callback retourne lui-même une promesse, qui sera fusionnée
    return p
 
# Ces callbacks ne retournent pas de promesses, et seront chainés
# normalement
@promise.then
def func3(v3):
    print(v3 + 1)
    return v3 + 1
 
@promise.then
def func4(v4):
    print(v4 + 1)
 
# Nos fonctions s'exécutent dans le bon ordre, mais bien de manière
# asynchrone par rapport au reste du programme.
 
## Je commence
## Juste après
## 1
## 2
## 3
## 4

Notez bien :

  • Le résultat “1” n’apparait que trois secondes après “Juste après”. Les fonctions sont donc bien non bloquantes.
  • Le resultat “2” apparait deux secondes après “1”: c’est aussi asynchrone, MAIS, n’est lancé que quand la première fonction a terminé son travail.
  • La deuxième fonction retourne une promesse, qui est fusionnée: tous ses callbacks vont s’exécuter en file avant que func3 soit lancé.

Évidement, n’utilisez pas cette implémentation de promise à la maison, c’est pédagogique. Ça ne gère pas les erreurs, ni le cas où le callback est enregistré après l’arrivée du résultat, et tout un tas d’autres cas tordus.

Syntaxe alternative

En Python, beaucoup de frameworks ont une approche plus agréable pour gérer les promesses à grand coup de yield. Twisted fait ça avec son @inlineCallback, asyncio avec @coroutine. C’est juste du sucre syntaxique pour vous rendre la vie plus facile.

Il s’agit de transformer une fonction en générateur, et à chaque fois qu’on appelle yield sur une promesse, elle est fusionnée avec la précédente. Ça donne presque l’impression d’écrire un code bloquant normal :

# un appel de fonction asyncrone typique de twisted
@inlineCallback
def une_fonction(data):
  data = yield func1(data)
  data = yield func2(data)
  data = yield func3(data)
 
une_fonction(truc)

Les fonctions 1, 2 et 3 vont ainsi être appelées de manière asynchrone par rapport au reste du programme, mais bien s’enchainer les unes à la suite des autres.

Ouai, tout ce bordel parce que l’asynchrone, c’est dur, donc on essaye de le faire ressembler à du code synchrone, qui lui est facile.

]]>
http://sametmax.com/deferred-future-et-promise-le-pourquoi-le-comment-et-quand-est-ce-quon-mange/feed/ 18
En attendant asyncio 15 http://sametmax.com/en-attendant-asyncio/ http://sametmax.com/en-attendant-asyncio/#comments Fri, 17 Jan 2014 14:09:59 +0000 http://sametmax.com/?p=8781 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.

]]>
http://sametmax.com/en-attendant-asyncio/feed/ 15