Je connais assez bien les composants std::thread
, std::async
et std::future
de C++ 11 (par exemple, voyez cette réponse ), qui sont directs.
Cependant, je ne peux pas tout à fait comprendre ce que std::promise
est ce qu'il fait et dans quelles situations il est le mieux utilisé. Le document standard lui-même ne contient pas beaucoup d'informations au-delà de son synopsis de classe, pas plus que just :: thread .
Quelqu'un pourrait-il donner un exemple succinct et succinct d'une situation dans laquelle un std::promise
est nécessaire et constitue la solution la plus idiomatique?
Dans les mots de [futures.state] un std::future
est un objet de retour asynchrone ("un objet qui lit les résultats d'un état partagé") et un std::promise
est un - fournisseur asynchrone ("un objet qui fournit un résultat à un état partagé"), c’est-à-dire qu’une promesse est la chose sur laquelle vous définissez un résultat, afin que vous puissiez get il du futur associé.
Le fournisseur asynchrone est ce qui crée initialement l'état partagé auquel un futur fait référence. std::promise
est un type de fournisseur asynchrone, std::packaged_task
en est un autre et le détail interne de std::async
en est un autre. Chacun de ceux-ci peut créer un état partagé et vous donner un std::future
qui partage cet état et peut le rendre prêt.
std::async
est un utilitaire de niveau supérieur qui vous fournit un objet de résultat asynchrone. Il 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 niveau, pour quand vous voulez transmettre un résultat asynchrone à l'avenir, mais le code qui le rend prêt ne peut pas être encapsulé dans une seule fonction appropriée pour passer à std::async
. Par exemple, vous pouvez avoir un tableau de plusieurs promise
s et associés future
s et avoir un seul thread qui effectue plusieurs calculs et définit un résultat sur chaque promesse. async
ne vous permet que de renvoyer un seul résultat, vous devez en appeler plusieurs (async
plusieurs fois, ce qui peut entraîner un gaspillage de ressources.
Je comprends un peu mieux la situation maintenant (en grande partie à cause des réponses fournies ici!), Alors j'ai pensé ajouter un petit article de ma part.
Il existe deux concepts distincts, quoique liés, dans C++ 11: le calcul asynchrone (une fonction appelée ailleurs) et l'exécution simultanée (un thread, quelque chose qui fonctionne simultanément). Les deux sont des concepts quelque peu orthogonaux. Le calcul asynchrone est simplement une variante différente de l'appel de fonction, alors 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 de la mise en œuvre.
Il existe une hiérarchie d'abstraction pour le calcul asynchrone. Par exemple, supposons que nous ayons une fonction qui prend quelques arguments:
_int foo(double, char, bool);
_
Tout d’abord, nous avons le modèle std::future<T>
, 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 doivent être considérés comme le remplacement instantané asynchrone des types de retour ordinaires. Pour notre exemple de fonction, nous attendons un _std::future<int>
_.
Passons maintenant à la hiérarchie, du plus haut niveau au plus bas:
std::async
: La méthode la plus simple et la plus simple pour effectuer un calcul asynchrone consiste à utiliser le modèle de fonction async
, qui renvoie immédiatement l'avenir correspondant:
_auto fut = std::async(foo, 1.5, 'x', false); // is a std::future<int>
_
Nous avons très peu de contrôle sur les détails. En particulier, nous ne savons 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
_
Nous pouvons maintenant considérer comment implémenter quelque chose comme async
, mais d'une manière que nous contrôlons. Par exemple, nous pouvons exiger que la fonction soit exécutée dans un thread séparé. Nous savons déjà que nous pouvons fournir un thread séparé au moyen de la classe std::thread
.
Le niveau d'abstraction inférieur suivant fait exactement cela: std::packaged_task
. Il s'agit d'un modèle qui enveloppe une fonction et fournit un futur pour la valeur de retour des fonctions, mais l'objet lui-même est appelable et l'appel est à la discrétion de l'utilisateur. Nous pouvons le configurer comme ceci:
_std::packaged_task<int(double, char, bool)> tsk(foo);
auto fut = tsk.get_future(); // is a std::future<int>
_
Le futur devient prêt une fois l'appel lancé et l'appel terminé. C'est le travail idéal pour un thread séparé. Nous devons juste nous assurer que déplacez la tâche dans le fil:
_std::thread thr(std::move(tsk), 1.5, 'x', false);
_
Le fil commence à courir immédiatement. Nous pouvons soit detach
soit, soit join
à la fin de la portée, ou à tout moment (par exemple, en utilisant le gestionnaire _scoped_thread
_ d'Anthony Williams, qui devrait réellement figurer dans la bibliothèque standard). Les détails d'utilisation de _std::thread
_ ne nous concernent pas ici; assurez-vous simplement de joindre ou de détacher 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
_
Nous en sommes maintenant au niveau le plus bas: comment pourrions-nous mettre en œuvre la tâche empaquetée? C'est ici que std::promise
entre en jeu. La promesse est la pierre angulaire de la communication avec le futur. Les principales étapes sont les suivantes:
Le fil d'appel fait une promesse.
Le fil d'appel obtient un avenir de la promesse.
La promesse, ainsi que les arguments de la fonction, sont déplacés dans un thread séparé.
Le nouveau thread exécute la fonction et remplit la promesse.
Le fil d'origine récupère le résultat.
À titre d'exemple, voici notre propre "tâche empaquetée":
_template <typename> class my_task;
template <typename R, typename ...Args>
class my_task<R(Args...)>
{
std::function<R(Args...)> fn;
std::promise<R> pr; // the promise of the result
public:
template <typename ...Ts>
explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
template <typename ...Ts>
void operator()(Ts &&... ts)
{
pr.set_value(fn(std::forward<Ts>(ts)...)); // fulfill the promise
}
std::future<R> 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 suppose de déplacer la promesse. Dans des situations plus ponctuelles, vous pouvez également déplacer explicitement un objet de promesse dans le nouveau thread et en faire un argument de fonction de la fonction de thread, mais un wrapper de tâche comme celui ci-dessus apparaît comme une solution plus souple et moins intrusive.
Les promesses sont intimement liées aux exceptions. L'interface d'une promesse à elle seule ne suffit pas pour exprimer complètement son état, aussi des exceptions sont-elles générées 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, seul n futur peut être obtenu!
Une promesse doit soit être satisfaite via set_value()
, soit faire l'objet d'une exception 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 pour le futur. Une promesse avec une exception lèvera l'exception stockée à l'appel de get()
dans le futur. Si la promesse décède sans valeur ni exception, appeler get()
à l'avenir déclenchera une exception "promesse non tenue".
Voici une petite série de tests pour démontrer ces différents comportements exceptionnels. Tout d'abord, le harnais:
_#include <iostream>
#include <future>
#include <exception>
#include <stdexcept>
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;
}
}
_
Passons maintenant aux tests.
Cas 1: promesse inactive
_int test()
{
std::promise<int> pr;
return 0;
}
// fine, no problems
_
Cas 2: Promesse active, non utilisée
_int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
return 0;
}
// fine, no problems; fut.get() would block indefinitely
_
Cas 3: trop de futures
_int test()
{
std::promise<int> pr;
auto fut1 = pr.get_future();
auto fut2 = pr.get_future(); // Error: "Future already retrieved"
return 0;
}
_
Cas 4: promesse satisfaite
_int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_value(10);
}
return fut.get();
}
// Fine, returns "10".
_
Cas 5: trop de satisfaction
_int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> 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 existe plus d'un des soit de _set_value
_ ou _set_exception
_.
Cas 6: Exception
_int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> 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 non tenue
_int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
} // Error: "broken promise"
return fut.get();
}
_
Bartosz Milewski fournit une bonne rédaction.
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 qui exécute une fonction au thread qui récupère la fonction future.
...
Un futur est l'objet de synchronisation construit autour de l'extrémité de réception du canal de promesse.
Donc, si vous voulez utiliser un avenir, vous vous retrouvez avec une promesse que vous utilisez pour obtenir le résultat du traitement asynchrone.
Un exemple de la page est:
promise<int> intPromise;
future<int> intFuture = intPromise.get_future();
std::thread t(asyncFun, std::move(intPromise));
// do some other stuff
int result = intFuture.get(); // may throw MyException
En gros, vous pouvez considérer std::promise
comme l'autre extrémité d'un std::future
(c'est false, mais à titre d'illustration, vous pouvez penser comme si c'était le cas). L'extrémité consommateur du canal de communication utiliserait un std::future
pour consommer la donnée de l'état partagé, tandis que le processus producteur utiliserait un std::promise
pour écrire dans l'état partagé.
std::promise
est le canal ou le chemin d'accès aux informations à renvoyer à partir de la fonction asynchrone. std::future
est le mécanisme de synchronisation qui fait attendre l'appelant jusqu'à ce que la valeur renvoyée dans le std::promise
soit prête (ce qui signifie que sa valeur est définie dans la fonction).
Il existe en réalité 3 entités principales dans le traitement asynchrone. C++ 11 se concentre actuellement sur 2 d'entre eux.
Les éléments essentiels dont vous avez besoin pour exécuter une logique de manière asynchrone sont les suivants:
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). Ceci est regrettable car les programmes réels doivent gérer les ressources de threads et de mémoire, et la plupart voudront que les tâches s’exécutent sur des pools de threads au lieu de créer et de détruire un thread pour chaque petite tâche (ce qui entraîne presque toujours des performances inutiles et permet de créer facilement des ressources. la famine est encore pire).
Selon Herb Sutter et d’autres personnes du groupe C++ 11, il est provisoirement envisagé d’ajouter un std::executor
qui, tout comme en Java, constituera la base des pools de threads et des configurations logiquement similaires pour (2). Peut-être que nous verrons cela en C++ 2014, mais mon pari est plus proche de C++ 17 (et que Dieu nous vienne en aide s'ils basent la norme en la matière).
Un std::promise
est créé en tant que point final d'une paire promesse/future et le std::future
(créé à partir de std :: promise à l'aide de la méthode get_future()
) est l'autre extrémité. Il s'agit d'une méthode simple et unique permettant aux deux threads de se synchroniser, un thread fournissant les données à un autre thread par le biais d'un message.
Vous pouvez le voir comme un fil crée une promesse de fournir des données et l'autre fil recueille la promesse dans le futur. Ce mécanisme ne peut être utilisé qu'une seule fois.
Le mécanisme promesse/avenir n’est qu’un sens, du thread qui utilise la méthode set_value()
d’un std::promise
au thread qui utilise la 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 plusieurs 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 dans un état d'attente jusqu'à ce que la promesse soit rempli par le premier thread avec le std::promise
quand il utilise la méthode set_value()
pour envoyer les données.
Avec les coroutines proposées de Spécification technique N4663 - Langages de programmation - Extensions C++ pour les Coroutines et le support du compilateur Visual Studio 2017 C++ de co_await
, il est également possible d'utiliser std::future
et std::async
pour écrire la fonctionnalité de coroutine. Voir la discussion et l'exemple dans https://stackoverflow.com/a/50753040/146697 qui contient une section décrivant l'utilisation de std::future
avec co_await
.
L'exemple de code suivant, une simple application de console Windows Visual Studio 2013, illustre l'utilisation de quelques classes/modèles de concurrence C++ 11 et d'autres fonctionnalités. Il illustre une utilisation qui fonctionne bien pour promesse/avenir, des threads autonomes qui effectuent certaines tâches et s’arrêtent, et une utilisation dans laquelle un comportement plus synchrone est requis et en raison de la nécessité de notifications multiples, la paire promesse/avenir ne fonctionne pas.
Une note à propos de cet exemple concerne les retards ajoutés à divers endroits. Ces retards ont été ajoutés uniquement pour que les différents messages imprimés sur la console à l'aide de std::cout
soient clairs et que le texte des différents threads ne soit 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 le moment où le fil principal démarre un fil, T2, qui attend les données du fil principal, fait quelque chose, puis envoie des données au troisième fil, T3, qui fait ensuite quelque chose et renvoie les données au fil principal.
La deuxième partie de la main()
crée deux threads et un ensemble de files d'attente pour autoriser plusieurs messages du thread principal à chacun des deux threads créés. Nous ne pouvons pas utiliser std::promise
et std::future
pour cela car le duo promesse/avenir ne fait qu'un, et ne peut pas être utilisé à plusieurs reprises.
Le code source de la classe Sync_queue
provient du langage de programmation C++: 4ème édition de Stroustrup.
// cpp_threads.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <thread> // std::thread is defined here
#include <future> // std::future and std::promise defined here
#include <list> // std::list which we use to build a message queue on.
static std::atomic<int> 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<typename Ttype>
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 <Ttype> q; // list that is used as a message queue
};
template<typename Ttype>
void Sync_queue<Ttype>::put(const Ttype &val) {
std::lock_guard <std::mutex> lck(mtx);
q.Push_back(val);
cond.notify_one();
}
template<typename Ttype>
void Sync_queue<Ttype>::get(Ttype &val) {
std::unique_lock<std::mutex> 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<int> &jj) {
int myId = std::atomic_fetch_add(&kount, 1); // get my identifier
std::future<int> 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<int> &jj, std::promise<int>&pp) {
int myId = std::atomic_fetch_add(&kount, 1); // get my identifier
std::future<int> 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<int> &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<int> 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<int> intPromiseT2;
std::promise<int> 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<int> 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<int> 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<int> q1; // notification queue for messages to thread t11
Sync_queue<int> 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 bout 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 fil, et vous ne créez même pas un fil "maintenant" - votre logiciel a peut-être été conçu pour choisir un fil dans un pool, de sorte que vous ne le sachiez pas qui effectuera le calcul à la fin.
Maintenant, que passez-vous à ce thread/classe/entité (encore inconnu)? Vous ne transmettez pas la future
, puisqu'il s'agit du résultat . Vous voulez passer quelque chose qui est connecté au future
et qui représente l'autre extrémité du fil, vous allez donc simplement interroger la future
sans savoir qui va réellement calculer/écrire quelque chose.
C'est le promise
. C'est un handle connecté à votre future
. Si le future
est un haut-parleur, et avec get()
vous commencez à écouter jusqu'à ce que du son soit émis, le promise
est un microphone ; mais pas n'importe quel microphone, c’est le microphone connecté par un seul fil au haut-parleur que vous tenez. Vous savez peut-être qui se trouve à l'autre bout du fil, mais vous n'avez pas besoin de le savoir, il vous suffit de le donner et d'attendre que l'autre partie dise quelque chose.
http://www.cplusplus.com/reference/future/promise/
Une explication de phrase: furture :: get () attend promse :: set_value () pour toujours.
void print_int(std::future<int>& fut) {
int x = fut.get(); // future would wait prom.set_value forever
std::cout << "value: " << x << '\n';
}
int main()
{
std::promise<int> prom; // create promise
std::future<int> fut = prom.get_future(); // engagement with future
std::thread th1(print_int, std::ref(fut)); // send future to new thread
prom.set_value(10); // fulfill promise
// (synchronizes with getting the future)
th1.join();
return 0;
}