Deferred, Future et Promise : le pourquoi, le comment, et quand est-ce qu’on mange


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.

19 thoughts on “Deferred, Future et Promise : le pourquoi, le comment, et quand est-ce qu’on mange

  • Pierre

    Ou sinon il y a Go! :P
    Le principe de ce langage est que “bloquer” ne coûte rien!

  • cym13

    Je pense avoir bien tout compris donc je suis content, d’autant que j’ai vraiment besoin de me mettre la tête dans les différentes façon de gérer l’asynchrone un de ses jours et que vous m’y aidez bien.

    Deux remarques quand même: je suis pas convaincu que mélanger “promesse” et “promise” au fil du tuto soit du plus clair pour un débutant complet. Aussi, je n’aime pas le javascript. Je suis tout à fait conscient de la démarche pédagogique à l’œuvre et du fait que le js est très au point sur l’asynchrone là où python pèche, mais je n’aime quand même pas ça. Vivement que vous soyez suffisamment à l’aise avec asyncio pour faire un vrai article dessus !

    Allez, comme je n’aime pas critiquer dans le vide, je vous offre un chat.

  • Anb

    Petite faute d’accord au début de l’article (sous le code imbitable)

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

    “Entrent” …

  • herison

    Salut,

    Juste pour être sur d’avoir pigé la fusion…

    Si j’ai 2 callbacks cb1 et cb2 sur une promise p1 et que cb1 retourne une promise p2 attachée à un autre callback cb3

    p1.then(cb1).then(cb2)
    Les choses vont se chaîner comme ceci cb1 -> cb3 -> cb2 ?

  • akersof

    C’est aussi super utile pour le traitement des erreurs lors d’une chaine de callback.
    Lorsqu’on fait du code asynchrone, donc travailler avec des I/O systeme, et qu’on chain les appels de callback on ne sait plus trop ou on en est déjà dans notre code, et encore pire dans le traitement de l’erreur, quelle callback qui a foirée, quelle erreur, etc
    Twisted offre la possibilité non seulement d’ajouter une callback aux deferred mais aussi ce qu’ils appellent une errback, meme principe qu’une callback mais ca sera une erreur :).
    Sous javascript ES6 va introduite dans le core les promises.
    Node.js avait au debut intégrer cela mais ils ont préféré le retirer du core et laisser des devs de libs les créer.
    Pour le javascript je recommande bluebird, qui est le trendy et qui monte qui monte, ou sinon Q (utilisable coté serveur et browsser)

    Bon je finis sur l’alternative evidement, certains ne veulent pas utiliser de promises car ils n’aiment pas ca.. sous node.js il y a le module async qui ne fonctionne pas avec le meme princpe d’un object qui est le deferred, mais rend l’ecriture d’un code asynchrone plus.. horizontal je dirais :)

    Voila utilisez les deferred/promises, dur au debut mais une fois qu’on sait s’en servir on en peut plus s’en passer :);
    On se rend compte de leur utilité lorsqu’on ecrit une grosse app aynchrone et les promises sont une maniere efficace d’eviter ce qu’on appelle dans le milieu “callback hell”

  • fab

    En parlant du module async en JavaScript, il y a la fonction auto qui est chouette et qui rend service.

  • Naouak

    Je veux pas être méchant mais :
    Les promises ne sont pas un truc de jQuery.

    Le premier code est infect non pas parce qu’il utilise des callback mais parce qu’il est juste mal pensé. Le callback-hell est assez souvent un problème de la part du développeur pas du principe. D’ailleurs le javascript à des mécanismes simple pour l’évite.

    En fait, juste en séparant simplement le code du premier en actions atomiques, on arrive ça :

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

    Je suis sûr qu’avec le vrai contexte de l’application on peut faire un meilleur truc que ça d’ailleurs.

    Le second exemple, est pas bon, les deux callbacks sont lancés en même temps et non l’un après l’autre. Il aurait fallut récupérer la nouvelle promise pour faire le second then.

    Le “plus beau code” utilise pas vraiment bien les promises.
    Voila une meilleure utilisation :

    $(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){
          return $.get('/session/ ' + session.id + '/context').then(loadContext);
        }
      })
      .then(function(){
        startApp(initUi);
      });
    });

    Quelle différence ? Les appels de then à partir du deuxième.
    Globalement, on finit par faire tout le temps la même chose. Pourquoi écrire deux fois l’appel quand on peut juste le chaîner ?

    Et pour finir, il me semble (je suis pas spécialiste en Python) que ta classe promise ne fait pas son boulot.
    L’un des avantages (occulté de cet article) des promises est qu’il permet aussi de stocker le résultat d’une opération asynchrone. Et donc si on ajoute un callback après que l’opération asynchrone soit finie, le callback est quand même lancé. Ça permet de mutualiser une opération asynchrone récurrente (comme par exemple à tout hasard une authentification).

    Au final, l’article montre pas vraiment les meilleurs points des promises ce qui est juste dommage. (le parrallélisme, le race, la gestion d’erreur, la réutilisation… d’ailleurs l’article de HTML5rocks montre des usages plus intéressants (Article sur les promises))

    PS: ce trombone est chiant.

  • Sam Post author

    @Naouak: tu es complètement passé à côté du fait que :

    – je ne dis pas que les promis vient de jquery, mais qu’on les retrouve dedans et aussi ailleurs. Je ne parle pas d’origine.
    – l’article est clairement pédagogique, le but n’est pas de faire au mieux mais de faire passer une notion. Les exemples sont volontairement imparfaits pour faire passer la notion.
    – pour l’implémentation Python, je dis en toute lettre à la fin que ça n’adresse pas ce problème.

    Bref, un tampon pour toi.

    Je subodore que tu es très jeune, puisque tu ne reconnais pas le trombonne. Ce qui explique sans doute le côté emporté.

  • Kyoku57

    Sympa l’article. J’ai tout compris :-) L’asynchrone est un monde bien particulier.

    PS : En parlant des tampons … en aurais-tu un à la sphaigne ou en en cheveux de cherokee ?

  • fab

    @Sam : en effet je me rend compte qu’une petite explication s’impose. La fonction auto permet de définir une suite de fonctions asynchrones à lancer sans se soucier de qui doit attendre quoi juste en définissant des dépendances entre elles.

    C’est assez bien expliqué ici : http://jakub.fedyczak.net/post/async-auto-best-feature-of-node-async/ avec un graphique qui permet de visualiser dans quel ordre vont s’exécuter les fonctions.

    Notamment on voit les chemins d’exécution en parallèle, dans l’exemple du lien il y en a trois qui se lancent dès que les dépendances sont exécutées, et qui se rejoignent à la fin pour obtenir le résultat voulu.

  • furankun

    De mon côté j’ai une question de noob qui n’a rien à voir: dans l’exempel python à ne pas reproduire tu utilises “@promise.then” pour ajouter une étape à ta chaîne de callback; je croyais qu’il fallait déclarer “then” en @classfunction pour pouvoir faire ça? oui je code en 2.7 et je ne comprends toujours rien aux @classfunction et @staticfuntion.

  • Duckie

    @Pierre : Le blocage est coûteux indépendamment du langage puisque le temps perdu provient d’éléments non maîtrisable au sein du logiciel (par exemple, une bande passante faible). C’est juste que Go force à réfléchir “par flux” et donc à faire de l’asynchrone sans qu’on s’en rende trop compte.

    @Sam: Pour la culture, C++11 voit l’introduction dans le standard C++ de std::future, std::promise, std::async, et toute la famille qui va bien. Ces concepts sont donc aussi présents en C++ en standard.

Comments are closed.

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