Je comprends un peu mieux la situation maintenant (en grande partie grâce aux réponses données ici !), alors j'ai pensé ajouter un petit article de mon cru.
Il existe deux concepts distincts, bien que liés, dans C++11 : le calcul asynchrone (une fonction qui est appelée ailleurs) et l'exécution simultanée (une fonction qui est appelée ailleurs). filetage quelque chose qui fonctionne en même temps). Ces deux concepts sont quelque peu orthogonaux. Le calcul asynchrone n'est qu'une autre forme d'appel de fonction, tandis qu'un thread est un contexte d'exécution. Les threads sont utiles en soi, mais dans le cadre de cette discussion, je les traiterai comme un détail de mise en œuvre.
Il existe une hiérarchie d'abstraction pour le calcul asynchrone. À titre d'exemple, supposons que nous ayons une fonction qui prend des 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 effectivement le programme en attendant le résultat. Alternativement, un futur supporte wait_for()
qui peut être utilisé pour vérifier si le résultat est déjà disponible ou non. Les Futures doivent être considérés comme le remplacement 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 au plus bas niveau :
-
std::async
: La façon la plus pratique et la plus directe d'effectuer un calcul asynchrone est d'utiliser la fonction async
qui renvoie immédiatement le futur 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 envisager comment mettre en œuvre quelque chose comme async
mais d'une manière qui nous contrôle. Par exemple, nous pouvons insister pour 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 fonction std::thread
classe.
C'est exactement ce que fait le niveau d'abstraction inférieur suivant : std::packaged_task
. Il s'agit d'un modèle qui enveloppe une fonction et fournit un futur pour la valeur de retour de la fonction, mais l'objet lui-même est appelable, et son appel est à la discrétion de l'utilisateur. Nous pouvons le configurer comme suit :
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 que nous appelons la tâche et que l'appel est terminé. C'est la tâche idéale pour un thread séparé. Nous devons juste nous assurer de déplacer la tâche dans le fil :
std::thread thr(std::move(tsk), 1.5, 'x', false);
Le fil commence à courir immédiatement. On peut soit detach
ou l'ont join
à la fin du champ d'application, ou à tout moment (par exemple, en utilisant la méthode d'Anthony Williams scoped_thread
qui devrait vraiment faire partie de la bibliothèque standard). Les détails de l'utilisation de std::thread
ne nous concernent pas ici, cependant ; assurez-vous simplement de rejoindre ou de détacher thr
éventuellement. Ce qui compte, c'est qu'à chaque fois que l'appel de la fonction se termine, notre résultat est prêt :
auto res = fut.get(); // as before
-
Maintenant, nous sommes au niveau le plus bas : comment pourrions-nous mettre en œuvre la tâche emballée ? C'est là que le std::promise
entre en jeu. La promesse est la pierre angulaire de la communication avec l'avenir. Les principales étapes sont les suivantes :
-
Le fil conducteur fait une promesse.
-
Le thread appelant obtient un futur 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 original récupère le résultat.
À titre d'exemple, voici notre propre "tâche emballé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'usage de ce modèle est essentiellement le même que celui de std::packaged_task
. Notez que le déplacement de la tâche entière subsume le déplacement de la promesse. Dans des situations plus ad hoc, on pourrait aussi déplacer un objet promesse explicitement dans le nouveau thread et en faire un argument de fonction de la fonction thread, mais un wrapper de tâche 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 ne suffit pas à elle seule à transmettre complètement son état, c'est pourquoi des exceptions sont lancées lorsqu'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, seuls un l'avenir peut être obtenu !
-
Une promesse doit être satisfaite soit par set_value()
ou avoir une exception définie via set_exception()
avant la fin de sa vie si l'on veut consommer son avenir. Une promesse satisfaite peut mourir sans conséquence, et get()
devient disponible à l'avenir. Une promesse avec une exception lèvera l'exception stockée lors de l'appel de get()
sur l'avenir. Si la promesse meurt sans valeur ni exception, appeler get()
sur l'avenir soulèvera une exception de "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 d'avenirs
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 y a plus d'un des éléments suivants soit de set_value
o 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();
}
1 votes
(Il y a d'autres choses que je ne comprends pas :
std::atomic_future
ystd::broken_promise
par exemple. Mais une chose à la fois).2 votes
Voici un peu de code avec elle : fr.cppreference.com/w/cpp/thread/future
0 votes
Avez-vous vérifié Wikipedia .. ?
68 votes
La version vraiment, vraiment courte est :
std::promise
c'est là oùstd::future
proviennent.std::future
est ce qui vous permet de récupérer une valeur qui a été promis à vous. Lorsque vous appelezget()
sur un futur, il attend que le propriétaire de l'objetstd::promise
avec lequel il définit la valeur (en appelantset_value
sur la promesse). Si la promesse est détruite avant qu'une valeur ne soit définie, et que vous appelez alorsget()
sur un futur associé à cette promesse, vous obtiendrez unestd::broken_promise
exception parce qu'on vous a promis une valeur, mais qu'il vous est impossible d'en obtenir une.0 votes
@chris : Merci, ça a dû être ajouté récemment ! Cette page était auparavant un stub... Ildjarn : Je vais la lire, merci !
1 votes
@JamesMcNellis : Je pensais que
std::async
c'est de là que viennent les futurs !0 votes
@KerrekSB, On dirait que les deux sont acceptables.
14 votes
Je vous recommande, si vous le pouvez/voulez, de jeter un coup d'œil à C++ Concurrence en action par Anthony Williams
37 votes
@KerrekSB
std::broken_promise
est le meilleur identifiant nommé de la bibliothèque standard. Et il n'y a passtd::atomic_future
.8 votes
Downvoter, pouvez-vous expliquer votre objection ?