Les futures

Une future est un objet représentant un éventuel résultat. Bien utilisée, une future peut accroître le parallélisme d'un programme.

En C++ std::future<T> et std::async()

Depuis C++ 11, il est possible d'utiliser des futures en C++. Un exemple simple simple comparant un calcul synchrone et un calcul équivalent mais réalisé de manière asynchrone, à l'aide de futures, suit.

Quelques en-têtes standards sont inclus. Nous utiliserons :

  • Les outils de <chrono> pour mesurer le temps et simuler des délais
  • Les outils de <future> pour... les futures, bien entendu, et
  • Les outils d'entrée/ sortie standards pour visualiser les résultats
#include <future>
#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;

Nous utiliserons deux calculs très (hum) sophistiqués, soit les fonctions f() et g() qui s'exécuteront respectivement en environ une seconde et environ deux secondes. L'idée ici sera de montrer les gains possibles avec une approche de calcul asynchrone, tout en mettant en relief l'accroissement (!) de la complexité de la tâche que ce virage implique.

int f(int n) {
   this_thread::sleep_for(seconds{1});
   return n + 1;
}
int g(int n) {
   this_thread::sleep_for(seconds{2});
   return n + 2;
}

La fonction calcul_synchrone(n) calculera de manière synchrone. Le temps total requis pour exécuter ce calcul sera essentiellement la somme des temps requis pour exécuter et .

La fonction calcul_asynchrone(n) calculera de manière asynchrone. Le temps total requis pour exécuter ce calcul sera essentiellement le maximum des temps requis pour exécuter et .

Ici, f_ et g_ sont des futures; leur type est std::future<int>, car chacune d'elles encapsule un calcul qui retournera un éventuel int.

Les calculs sont lancés par std::async(), qui accepte une opération et ses paramètres, puis effectue le calcul associé de manière asynchrone. La méthode get() bloque jusqu'à ce qu'un résultat devienne disponible, ou jusqu'à ce qu'une exception soit levée, et retourne ce résultat (ou re-lève cette exception, selon le cas).

int calcul_synchrone(int n) {
   return f(n) + g(n);
}
int calcul_asynchrone(int n) {
   auto f_ = async(f, n);
   auto g_ = async(g, n);
   return f_.get() + g_.get();
}

La fonction tester() semble complexe, car elle repose sur des mécanismes variadiques, mais ce qu'il faut savoir d'elle est qu'elle prend une fonction , ses paramètres (il peut y en avoir plusieurs), puis calcule le temps requis pour exécuter . La fonction tester() en tant que telle retourne une paire dont le premier élément est le résultat du calcul, et dont le second élément est le temps écoulé. En pratique, elle est très facile à utiliser (voir ci-dessous).

template<class F, class ... Args>
   auto tester(F f, Args && ... args) -> pair<decltype(f(std::forward<Args>(args)...)), system_clock::duration>    {
      auto avant = system_clock::now();
      auto res = f(std::forward<Args>(args)...);
      return make_pair(move(res), system_clock::now() - avant);
   }

Enfin, le programme de test calcule de manière synchrone, puis de manière asynchrone, et affiche à la fois les résultats (identiques) et le temps requis pour y arriver (différent, évidemment).

int main() {
   auto sync_dt = tester(calcul_synchrone, 1);
   cout << "Calcul synchrone, resultat : " << sync_dt.first << " obtenu en "
        << duration_cast<milliseconds>(sync_dt.second).count() << " ms.\n";
   auto async_dt = tester(calcul_asynchrone, 1);
   cout << "Calcul asynchrone, resultat : " << sync_dt.first << " obtenu en "
        << duration_cast<milliseconds>(async_dt.second).count() << " ms." << endl;
}

Une exécution possible serait :

Calcul synchrone, resultat : 5 obtenu en 3000 ms.
Calcul asynchrone, resultat : 5 obtenu en 2003 ms.

En C# – les expressions async et await

Les futures avec C# sont implémentées à partir de fonctions dites « résumables », à l'aide des mots clés async et await. Pour en savoir plus, voir ../Divers--cdiese/async_await.html

Lectures complémentaires

Pour un exemple de future « maison », pré-C++ 11, par votre humble serviteur, voir ../Client-Serveur/futures-maison.html.

Évidemment, ces expérimentations sont amusantes mais ne remplacent pas les outils standards, que je vous invite à privilégier.

Un Wiki sur le sujet : http://en.wikipedia.org/wiki/Futures_and_promises

La position de Marius A. Eriksen en 2013 : http://aboutwhichmorelater.tumblr.com/post/46862769549/futures-arent-ersatz-threads

Les futures et les promesses avec iOS (mais au fond, ce texte ratisse beaucoup plus large et ne se limite pas à une plateforme particulière), texte de Drew Crawford en 2013 : http://sealedabstract.com/code/broken-promises/

En 2016, Raymond Chen montre comment il est possible de transformer une boucle en un enchaînement de tâches : https://blogs.msdn.microsoft.com/oldnewthing/20160226-00/?p=93092

Avec C# :

Avec C++ :

Bloquer ou non à la destruction?

Les futures générées par std::async() de C++ 11 bloquent à la destruction jusqu'à ce que le thread qui leur est associé complète sa tâche, un comportement qui, en surface du moins, diffère de celles créées par d'autres moyens. Ceci peut irriter les programmeuses et les programmeurs, et amène discussions et réflexions en vue de C++ 14 dans l'optique d'harmoniser les effets de ces diverses approches.

// ...
{
   std::async(std::launch::async, []{ f(); });
   std::async(std::launch::async, []{ g(); });
} // ici, la 2e ligne ne s'exécute pas tant que f() n'a pas complété

 À ce sujet :

Avec Go :

Avec Java :

Avec JavaScript :


Valid XHTML 1.0 Transitional

CSS Valide !