Qu’est-ce que std :: promesse?

Je suis assez familier avec std::future composants std::thread , std::async et std::future C ++ 11 (voir par exemple cette réponse ), qui sont simples.

Cependant, je ne peux pas comprendre ce que std::promise est, ce qu’il fait et dans quelles situations il est préférable de l’utiliser. Le document standard lui-même ne contient pas beaucoup d’informations au-delà de son synopsis de classes, et ne fait pas que :: thread .

Quelqu’un pourrait-il donner un bref exemple succinct d’une situation où une std::promise est nécessaire et où c’est la solution la plus idiomatique?

Dans les mots de [futures.state] un std::future est un object de retour asynchrone (“un object qui lit les résultats d’un état partagé”) et un std::promise est un fournisseur asynchrone (“un object qui fournit un résultat à un état partagé “) c’est-à-dire une promesse est la chose sur laquelle vous définissez un résultat, de sorte que vous puissiez l’ obtenir du futur associé.

Le fournisseur asynchrone est ce qui crée initialement l’état partagé auquel un futur se réfère. std::promise est un type de fournisseur asynchrone, std::packaged_task est un autre et le détail interne de std::async est un autre. Chacun d’eux peut créer un état partagé et vous donner un std::future qui partage cet état et peut préparer l’état.

std::async est un utilitaire de commodité de niveau supérieur qui vous fournit un object de résultat asynchrone et prend en charge en interne la création du fournisseur asynchrone et la préparation de l’état partagé à la fin de la tâche. Vous pouvez l’émuler avec un std::packaged_task (ou std::bind et un std::promise ) et un std::thread mais il est plus sûr et plus facile d’utiliser std::async .

std::promise est un peu plus bas, car lorsque vous voulez passer un résultat asynchrone au futur, le code qui rend le résultat prêt ne peut pas être intégré dans une seule fonction appropriée pour passer à std::async . Par exemple, vous pouvez avoir un tableau de plusieurs promise et future associés et avoir un seul thread qui effectue plusieurs calculs et définit un résultat pour chaque promesse. async ne vous permettrait de renvoyer qu’un seul résultat, pour en renvoyer plusieurs que vous auriez besoin d’appeler plusieurs fois en async , ce qui pourrait gaspiller des ressources.

Je comprends un peu mieux la situation maintenant (en grande partie à cause des réponses ici!), Alors j’ai pensé append moi-même un petit article.


Il existe deux concepts distincts, bien que connexes, en C ++ 11: le calcul asynchrone (une fonction appelée ailleurs) et l’exécution simultanée (un thread , quelque chose qui fonctionne en même temps). Les deux sont des concepts quelque peu orthogonaux. Le calcul asynchrone est simplement une variante de l’appel de la fonction, tandis qu’un thread est un contexte d’exécution. Les threads sont utiles en eux-mêmes, mais pour les besoins de cette discussion, je les traiterai comme un détail d’implémentation.

Il y a une hiérarchie d’abstraction pour le calcul asynchrone. Par exemple, supposons que nous ayons une fonction qui prenne des arguments:

 int foo(double, char, bool); 

Tout d’abord, nous avons le modèle std::future , qui représente une valeur future de type T La valeur peut être récupérée via la fonction membre get() , qui synchronise efficacement le programme en attendant le résultat. Sinon, un futur supporte wait_for() , qui peut être utilisé pour déterminer si le résultat est déjà disponible ou non. Les contrats à terme devraient être considérés comme le remplacement asynchrone des types de retour ordinaires. Pour notre exemple de fonction, nous attendons un std::future .

Maintenant, à la hiérarchie, du plus haut au plus bas niveau:

  1. std::async : Le moyen le plus pratique et le plus simple d’effectuer un calcul asynchrone est d’ async modèle de fonction async , qui renvoie immédiatement le futur correspondant:

     auto fut = std::async(foo, 1.5, 'x', false); // is a std::future 

    Nous avons très peu de contrôle sur les détails. En particulier, nous ne soaps même pas si la fonction est exécutée simultanément, en série sur get() , ou par une autre magie noire. Cependant, le résultat est facilement obtenu en cas de besoin:

     auto res = fut.get(); // is an int 
  2. Nous pouvons maintenant envisager comment implémenter quelque chose comme async , mais d’une manière que nous contrôlons. Par exemple, nous pouvons insister pour que la fonction soit exécutée dans un thread séparé. Nous soaps déjà que nous pouvons fournir un thread séparé au moyen de la classe std::thread .

    Le prochain niveau d’abstraction inférieur fait exactement cela: std::packaged_task . Ceci est un modèle qui enveloppe une fonction et fournit un futur pour la valeur de retour des fonctions, mais l’object lui-même est appelable, et l’appel est à la discrétion de l’utilisateur. Nous pouvons le configurer comme ceci:

     std::packaged_task tsk(foo); auto fut = tsk.get_future(); // is a std::future 

    L’avenir devient prêt une fois que nous appelons la tâche et que l’appel se termine. C’est le travail idéal pour un thread séparé. Nous devons juste nous assurer de déplacer la tâche dans le thread:

     std::thread thr(std::move(tsk), 1.5, 'x', false); 

    Le thread commence à s’exécuter immédiatement. Nous pouvons soit le detach , soit le join à la fin de la scope, ou chaque fois (par exemple, en utilisant le wrapper scoped_thread Anthony Williams, qui devrait vraiment se trouver dans la bibliothèque standard). Les détails de l’utilisation de std::thread ne nous concernent pas ici, cependant; Assurez-vous simplement de vous joindre ou de vous thr éventuellement. Ce qui compte, c’est que chaque fois que l’appel de fonction se termine, notre résultat est prêt:

     auto res = fut.get(); // as before 
  3. Maintenant, nous sums au plus bas niveau: comment allons-nous mettre en œuvre la tâche packagée? C’est là std::promise le std::promise . La promesse est la pierre angular de la communication avec le futur. Les principales étapes sont les suivantes:

    • Le thread appelant fait une promesse.

    • Le thread appelant obtient un avenir à partir de la promesse.

    • La promesse, ainsi que les arguments de fonction, sont déplacés dans un thread séparé.

    • Le nouveau thread exécute la fonction et remplit la promesse.

    • Le thread d’origine récupère le résultat.

    Par exemple, voici notre propre “tâche packagée”:

     template  class my_task; template  class my_task { std::function fn; std::promise pr; // the promise of the result public: template  explicit my_task(Ts &&... ts) : fn(std::forward(ts)...) { } template  void operator()(Ts &&... ts) { pr.set_value(fn(std::forward(ts)...)); // fulfill the promise } std::future get_future() { return pr.get_future(); } // disable copy, default move }; 

    L’utilisation de ce modèle est essentiellement la même que celle de std::packaged_task . Notez que déplacer la tâche entière subsume en déplaçant la promesse. Dans des situations plus ponctuelles, on pourrait aussi déplacer explicitement un object prometteur dans le nouveau thread et en faire un argument de fonction de la fonction thread, mais un wrapper de tâches comme celui ci-dessus semble être une solution plus flexible et moins intrusive.


Faire des exceptions

Les promesses sont intimement liées aux exceptions. L’interface d’une promesse seule ne suffit pas à transmettre complètement son état, donc des exceptions sont émises chaque fois qu’une opération sur une promesse n’a pas de sens. Toutes les exceptions sont de type std::future_error , qui dérive de std::logic_error . Tout d’abord, une description de certaines contraintes:

  • Une promesse construite par défaut est inactive. Les promesses inactives peuvent mourir sans conséquence.

  • Une promesse devient active lorsqu’un futur est obtenu via get_future() . Cependant, un seul avenir peut être obtenu!

  • Une promesse doit être satisfaite via set_value() ou une exception doit être définie via set_exception() avant la fin de sa durée de vie si son avenir doit être consommé. Une promesse satisfaite peut mourir sans conséquence et get() devient disponible dans le futur. Une promesse avec une exception déclenchera l’exception stockée lors de l’appel de get() sur le futur. Si la promesse meurt sans valeur ni exception, l’appel de get() à l’avenir provoquera une exception de “promesse non respectée”.

Voici une petite série de tests pour démontrer ces différents comportements exceptionnels. Tout d’abord, le harnais:

 #include  #include  #include  #include  int test(); int main() { try { return test(); } catch (std::future_error const & e) { std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl; } catch (std::exception const & e) { std::cout << "Standard exception: " << e.what() << std::endl; } catch (...) { std::cout << "Unknown exception." << std::endl; } } 

Maintenant aux tests.

Cas 1: promesse inactive

 int test() { std::promise pr; return 0; } // fine, no problems 

Cas 2: promesse active, non utilisée

 int test() { std::promise pr; auto fut = pr.get_future(); return 0; } // fine, no problems; fut.get() would block indefinitely 

Cas 3: Trop d'avenir

 int test() { std::promise pr; auto fut1 = pr.get_future(); auto fut2 = pr.get_future(); // Error: "Future already resortingeved" return 0; } 

Cas 4: promesse satisfaite

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_value(10); } return fut.get(); } // Fine, returns "10". 

Cas 5: Trop de satisfaction

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_value(10); pr2.set_value(10); // Error: "Promise already satisfied" } return fut.get(); } 

La même exception est levée s'il y a plusieurs set_value de set_value ou set_exception .

Cas 6: Exception

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo"))); } return fut.get(); } // throws the runtime_error exception 

Cas 7: promesse brisée

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); } // Error: "broken promise" return fut.get(); } 

Bartosz Milewski fournit une bonne écriture.

C ++ divise l’implémentation des futures en un ensemble de petits blocs

std :: promise est l’une de ces parties.

Une promesse est un moyen de transmettre la valeur de retour (ou une exception) du thread exécutant une fonction au thread qui encaisse sur la fonction future.

Un futur est l’object de synchronisation construit autour de l’extrémité récepsortingce du canal de promesse.

Donc, si vous voulez utiliser un futur, vous vous retrouvez avec une promesse que vous utilisez pour obtenir le résultat du traitement asynchrone.

Un exemple de la page est:

 promise intPromise; future intFuture = intPromise.get_future(); std::thread t(asyncFun, std::move(intPromise)); // do some other stuff int result = intFuture.get(); // may throw MyException 

Dans une approximation approximative, vous pouvez considérer std::promise comme l’autre extrémité d’un std::future (c’est faux , mais pour illustration, vous pouvez penser comme si c’était). Le consommateur final du canal de communication utiliserait un std::future pour consumr le datum de l’état partagé, tandis que le thread producteur utiliserait un std::promise pour écrire dans l’état partagé.

std::promise est le canal ou le chemin des informations à renvoyer de la fonction asynchrone. std::future est le mécanisme de synchronisation qui permet à l’appelant d’attendre que la valeur renvoyée dans std::promise soit prête (ce qui signifie que sa valeur est définie dans la fonction).

Il y a vraiment 3 entités principales dans le traitement asynchrone. C ++ 11 se concentre actuellement sur 2 d’entre eux.

Les éléments essentiels pour exécuter une logique de manière asynchrone sont les suivants:

  1. La tâche (logique empaquetée sous forme d’object foncteur) qui s’exécutera «quelque part».
  2. Le nœud de traitement proprement dit – un thread, un processus, etc. qui exécutent de tels foncteurs lorsqu’ils sont fournis. Regardez le modèle de conception “Command” pour avoir une bonne idée de la façon dont un pool de threads de travail de base effectue cette opération.
  3. Le handle de résultat : Quelqu’un a besoin de ce résultat et a besoin d’un object qui le récupérera. Pour la POO et d’autres raisons, toute attente ou synchronisation doit être effectuée dans les API de ce handle.

C ++ 11 appelle les choses dont je parle dans (1) std::promise , et celles dans (3) std::future . std::thread est la seule chose fournie publiquement pour (2). C’est regrettable car les vrais programmes ont besoin de gérer les ressources thread et mémoire, et la plupart voudront exécuter des tâches sur des pools de threads au lieu de créer et détruire un thread pour chaque petite tâche (ce qui entraîne presque toujours la famine qui est encore pire.

Selon Herb Sutter et d’autres personnes de la confiance cérébrale C ++ 11, il est prévu d’append un std::executor comme en Java, servira de base aux pools de threads et aux configurations logiquement similaires pour (2). Peut-être le verrons-nous en C ++ 2014, mais mon pari ressemble plus à C ++ 17 (et Dieu nous aide s’ils bousculent le standard).

Un std::promise est créé en tant que point final pour une paire promis / future et le std::future (créé à partir de la méthode std :: promise en utilisant la méthode get_future() ) est l’autre point final. Il s’agit d’une méthode simple qui permet à deux threads de se synchroniser au fur et à mesure qu’un thread fournit des données à un autre thread via un message.

Vous pouvez penser à cela comme un thread crée une promesse de fournir des données et l’autre thread recueille la promesse dans le futur. Ce mécanisme ne peut être utilisé qu’une seule fois.

Le mécanisme promis / futur n’est qu’une direction, à partir du thread qui utilise la méthode set_value() d’un std::promise sur le thread qui utilise la méthode get() d’un std::future pour recevoir les données. Une exception est générée si la méthode get() d’un futur est appelée plus d’une fois.

Si le thread avec le std::promise n’a pas utilisé set_value() pour remplir sa promesse, lorsque le second thread appelle get() du std::future pour collecter la promesse, le second thread passera en attente jusqu’à ce que La promesse est remplie par le premier thread avec la méthode std::promise quand elle utilise la méthode set_value() pour envoyer les données.

L’exemple de code suivant, une simple application de console Visual Studio 2013 Windows, montre l’utilisation de quelques-unes des classes / modèles de concurrence C ++ 11 et d’autres fonctionnalités. Il illustre une utilisation prometteuse / future qui fonctionne bien, des threads autonomes qui effectuent des tâches et s’arrêtent, et une utilisation où un comportement plus synchrone est requirejs et en raison de la nécessité de notifications multiples, la paire promesse / future ne fonctionne pas.

Une remarque à propos de cet exemple concerne les retards ajoutés à divers endroits. Ces retards ont été ajoutés uniquement pour s’assurer que les différents messages imprimés sur la console à l’aide de std::cout seraient clairs et que le texte des différents threads ne serait pas mélangé.

La première partie de main() crée trois threads supplémentaires et utilise std::promise et std::future pour envoyer des données entre les threads. Un point intéressant est celui où le thread principal lance un thread, T2, qui attendra des données du thread principal, fera quelque chose, puis enverra des données au troisième thread, T3, qui fera quelque chose et renverra des données au fil principal

La deuxième partie de main() crée deux threads et un ensemble de files d’attente pour autoriser plusieurs messages du thread principal vers chacun des deux threads créés. Pour cela, nous ne pouvons pas utiliser std::promise et std::future car le duo promesse / futur ne peut être utilisé qu’une seule fois.

La source de la classe Sync_queue provient du langage de programmation C ++ de Stroustrup: 4ème édition.

 // cpp_threads.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include  #include  // std::thread is defined here #include  // std::future and std::promise defined here #include  // std::list which we use to build a message queue on. static std::atomic kount(1); // this variable is used to provide an identifier for each thread started. //------------------------------------------------ // create a simple queue to let us send notifications to some of our threads. // a future and promise are one shot type of notifications. // we use Sync_queue<> to have a queue between a producer thread and a consumer thread. // this code taken from chapter 42 section 42.3.4 // The C++ Programming Language, 4th Edition by Bjarne Stroustrup // copyright 2014 by Pearson Education, Inc. template class Sync_queue { public: void put(const Ttype &val); void get(Ttype &val); private: std::mutex mtx; // mutex used to synchronize queue access std::condition_variable cond; // used for notifications when things are added to queue std::list  q; // list that is used as a message queue }; template void Sync_queue::put(const Ttype &val) { std::lock_guard  lck(mtx); q.push_back(val); cond.notify_one(); } template void Sync_queue::get(Ttype &val) { std::unique_lock lck(mtx); cond.wait(lck, [this]{return !q.empty(); }); val = q.front(); q.pop_front(); } //------------------------------------------------ // thread function that starts up and gets its identifier and then // waits for a promise to be filled by some other thread. void func(std::promise &jj) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future intFuture(jj.get_future()); auto ll = intFuture.get(); // wait for the promise attached to the future std::cout << " func " << myId << " future " << ll << std::endl; } // function takes a promise from one thread and creates a value to provide as a promise to another thread. void func2(std::promise &jj, std::promise&pp) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future intFuture(jj.get_future()); auto ll = intFuture.get(); // wait for the promise attached to the future auto promiseValue = ll * 100; // create the value to provide as promised to the next thread in the chain pp.set_value(promiseValue); std::cout << " func2 " << myId << " promised " << promiseValue << " ll was " << ll << std::endl; } // thread function that starts up and waits for a series of notifications for work to do. void func3(Sync_queue &q, int iBegin, int iEnd, int *pInts) { int myId = std::atomic_fetch_add(&kount, 1); int ll; q.get(ll); // wait on a notification and when we get it, processes it. while (ll > 0) { std::cout << " func3 " << myId << " start loop base " << ll << " " << iBegin << " to " << iEnd << std::endl; for (int i = iBegin; i < iEnd; i++) { pInts[i] = ll + i; } q.get(ll); // we finished this job so now wait for the next one. } } int _tmain(int argc, _TCHAR* argv[]) { std::chrono::milliseconds myDur(1000); // create our various promise and future objects which we are going to use to synchronise our threads // create our three threads which are going to do some simple things. std::cout << "MAIN #1 - create our threads." << std::endl; // thread T1 is going to wait on a promised int std::promise intPromiseT1; std::thread t1(func, std::ref(intPromiseT1)); // thread T2 is going to wait on a promised int and then provide a promised int to thread T3 std::promise intPromiseT2; std::promise intPromiseT3; std::thread t2(func2, std::ref(intPromiseT2), std::ref(intPromiseT3)); // thread T3 is going to wait on a promised int and then provide a promised int to thread Main std::promise intPromiseMain; std::thread t3(func2, std::ref(intPromiseT3), std::ref(intPromiseMain)); std::this_thread::sleep_for(myDur); std::cout << "MAIN #2 - provide the value for promise #1" << std::endl; intPromiseT1.set_value(22); std::this_thread::sleep_for(myDur); std::cout << "MAIN #2.2 - provide the value for promise #2" << std::endl; std::this_thread::sleep_for(myDur); intPromiseT2.set_value(1001); std::this_thread::sleep_for(myDur); std::cout << "MAIN #2.4 - set_value 1001 completed." << std::endl; std::future intFutureMain(intPromiseMain.get_future()); auto t3Promised = intFutureMain.get(); std::cout << "MAIN #2.3 - intFutureMain.get() from T3. " << t3Promised << std::endl; t1.join(); t2.join(); t3.join(); int iArray[100]; Sync_queue q1; // notification queue for messages to thread t11 Sync_queue q2; // notification queue for messages to thread t12 std::thread t11(func3, std::ref(q1), 0, 5, iArray); // start thread t11 with its queue and section of the array std::this_thread::sleep_for(myDur); std::thread t12(func3, std::ref(q2), 10, 15, iArray); // start thread t12 with its queue and section of the array std::this_thread::sleep_for(myDur); // send a series of jobs to our threads by sending notification to each thread's queue. for (int i = 0; i < 5; i++) { std::cout << "MAIN #11 Loop to do array " << i << std::endl; std::this_thread::sleep_for(myDur); // sleep a moment for I/O to complete q1.put(i + 100); std::this_thread::sleep_for(myDur); // sleep a moment for I/O to complete q2.put(i + 1000); std::this_thread::sleep_for(myDur); // sleep a moment for I/O to complete } // close down the job threads so that we can quit. q1.put(-1); // indicate we are done with agreed upon out of range data value q2.put(-1); // indicate we are done with agreed upon out of range data value t11.join(); t12.join(); return 0; } 

Cette application simple crée la sortie suivante.

 MAIN #1 - create our threads. MAIN #2 - provide the value for promise #1 func 1 future 22 MAIN #2.2 - provide the value for promise #2 func2 2 promised 100100 ll was 1001 func2 3 promised 10010000 ll was 100100 MAIN #2.4 - set_value 1001 completed. MAIN #2.3 - intFutureMain.get() from T3. 10010000 MAIN #11 Loop to do array 0 func3 4 start loop base 100 0 to 5 func3 5 start loop base 1000 10 to 15 MAIN #11 Loop to do array 1 func3 4 start loop base 101 0 to 5 func3 5 start loop base 1001 10 to 15 MAIN #11 Loop to do array 2 func3 4 start loop base 102 0 to 5 func3 5 start loop base 1002 10 to 15 MAIN #11 Loop to do array 3 func3 4 start loop base 103 0 to 5 func3 5 start loop base 1003 10 to 15 MAIN #11 Loop to do array 4 func3 4 start loop base 104 0 to 5 func3 5 start loop base 1004 10 to 15 

La promesse est l’autre extrémité du fil.

Imaginez que vous ayez besoin de récupérer la valeur d’un future calculé par un async . Cependant, vous ne voulez pas qu’il soit calculé dans le même thread et vous ne lancez même pas un thread “maintenant” – peut-être que votre logiciel a été conçu pour sélectionner un thread dans un pool, vous ne savez donc pas qui le fera. effectuer le calcul à la fin.

Maintenant, que transmettez-vous à ce fil / cette classe / cette entité (encore inconnue)? Vous ne passez pas le future , car c’est le résultat . Vous voulez passer quelque chose qui est connecté au future et qui représente l’autre bout du fil , alors vous allez juste interroger le future sans savoir qui va réellement calculer / écrire quelque chose.

C’est la promise . C’est une poignée connectée à votre future . Si le future est un hautparleur , et avec get() vous commencez à écouter jusqu’à ce qu’un son sorte, la promise est un microphone ; mais pas n’importe quel microphone, c’est le microphone connecté avec un seul fil au haut-parleur que vous tenez. Vous savez peut-être qui est à l’autre bout mais vous n’avez pas besoin de le savoir – vous le donnez simplement et attendez que l’autre partie dise quelque chose.