Je vous recommande d'avoir lu et compris l'article sur les futures avant de lire ce qui suit.
L'un des objectifs des systèmes parallèles et concurrents est la maximisation de leur débit :
C'est le « sans attendre » qui nous intéressera ici. Pour combattre les frontières placées par la loi d'Amdahl ou la loi de Gustafson, nous voulons réduire les fractions séquentielles de nos programmes. Cela signifie en général réduire les blocages et les mises en attente de nos threads.
Les continuations sont l'une des principales approches connues pour approcher cet objectif. Par continuation, on entend indiquer ce par quoi il faut qu'une tâche asynchrone (en C++, une future) enchaîne suite à sa complétion.
En vue de C++ 17, on parlera informellement de l'approche future...then puisqu'il s'agit des mots qui ont été choisis pour y représenter les continuations, mais le concept est plus large que cela et existe dans plusieurs langages.
Ce dont nous discutons ci-dessous fait partie de la spécification technique sur la concurrence de C++ telle que prévue pour expérimentation, probablement en vue d'une inclusion dans le standard à partir de C++ 17.
L'idée est simple, soit remplacer des programmes comme celui-ci :
// ...
int f();
double g(int);
// ...
auto fut0 = async([]() { return f(); });
// ...
// Ici, fut0.get() bloquera potentiellement, puis retournera un int ou lèvera une exception
auto fut1 = async([](int n) { return g(n); }, fut0.get());
// Ici, fut1.get() bloquera potentiellement, puis retournera un double ou lèvera une exception
cout << fut1.get() << endl;
... par des programmes comme celui-là :
// ...
int f();
double g(int);
// ...
auto fut = async(
[]() { return f(); }
).then(
[](future<int> fut) { return g(fut.get()); }
);
// ...
// Ici, fut.get() bloquera potentiellement, puis retournera un double ou lèvera une exception
cout << fut.get() << endl;
Remarquez la nuance : dans le deuxième cas, le seul blocage surviendra lors du get() de la future composite (variable fut). Lorsque la future résultat du async() complètera, sa valeur de retour sera encapsulée dans une future et passée en paramètre au then, qui la prendra en charge à ce moment et retournera une autre future (et ainsi de suite).
C'est ce qu'on appelle des continuations : par la combinaison d'une succession de then, les exécutions s'enchaîneront de manière automatique sans bloquer un thread tiers, jusqu'à complétion de la séquence d'enchaînements planifiée.
Les continuations sont un enrichissement apporté aux future et aux shared_future. La gamme de services de ces deux classes demeure la même, mais avec les ajustements suivants :
Les remarques sur les types de retour impliquent aussi la gestion des exceptions. Comme à l'habitude, une future encapsulera à la fois la valeur de retour et une possible exception, levée par un appel à sa méthode get(). Ainsi, une exception levée quelque part dans la chaîne des continuations fera son chemin jusqu'à la future placée entre les mains du code client.
Pour alléger la mécanique, ces nouveaux outils s'accompagnent de quelques fonctionnalités auxiliaires :
Bien que ces fonctions soient accessibles à tous et à toutes, leur principal rôle et de faciliter la mécanique des continuations.
Quelques liens pour enrichir le propos.