Je comprends un peu mieux la situation maintenant (en grande partie à cause des réponses ici!), Alors j'ai pensé ajouter une petite rédaction de ma part.
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 (un thread , quelque chose qui fonctionne simultanément). Les deux sont des concepts quelque peu orthogonaux. Le calcul asynchrone est juste une variante différente de l'appel de fonction, tandis qu'un thread est un contexte d'exécution. Les threads sont utiles en soi, mais pour les besoins de cette discussion, je les traiterai comme un détail d'implémentation.
Il existe une hiérarchie d'abstraction pour le calcul asynchrone. Par exemple, supposons que nous ayons une fonction qui accepte certains 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. Alternativement, un futur support wait_for()
, qui peut être utilisé pour sonder si oui ou non le résultat est déjà disponible. Les contrats à terme devraient être considérés comme le remplacement asynchrone des types de retours ordinaires. Pour notre exemple de fonction, nous attendons a std::future<int>
.
Maintenant, passons à la hiérarchie, du plus haut au plus bas:
std::async
: Le moyen le plus pratique et le plus simple d'effectuer un calcul asynchrone est via le async
modèle de fonction, 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 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 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 std::thread
classe.
Le niveau inférieur de l' abstraction fait exactement cela: std::packaged_task
. Il s'agit d'un modèle qui encapsule une fonction et offre un avenir 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>
L'avenir devient prêt une fois que nous avons appelé la tâche et que l'appel est terminé. 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 démarre immédiatement. Nous pouvons le detach
faire, ou l'avoir join
à la fin de la portée, ou à tout moment (par exemple en utilisant le scoped_thread
wrapper d' Anthony Williams , qui devrait vraiment être dans la bibliothèque standard). Les détails d'utilisation std::thread
ne nous concernent cependant pas ici; assurez-vous simplement de vous joindre ou de vous détacher thr
éventuellement. Ce qui importe, c'est que chaque fois que l'appel de 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 implémenter la tâche packagée? C'est là std::promise
qu'intervient. La promesse est la pierre angulaire de la communication avec un avenir. Les principales étapes sont les suivantes:
Le thread appelant fait une promesse.
Le thread appelant obtient un avenir 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.
À titre d'exemple, voici notre propre "tâche packagé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 le déplacement de la tâche entière résume le déplacement de la promesse. Dans des situations plus ad hoc, on pourrait également déplacer un objet de promesse explicitement 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 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 pour transmettre complètement son état, de sorte que des exceptions sont levé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 avenir est obtenu via get_future()
. Cependant, un seul avenir peut être obtenu!
Une promesse doit être satisfaite via set_value()
ou avoir une exception 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 à l'avenir. Une promesse avec une exception lèvera l'exception stockée lors d'un appel get()
à l'avenir. Si la promesse ne meurt sans valeur ni exception, faire appel get()
à l'avenir entraînera une exception de "promesse rompue".
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 futurs
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 ou l' autre 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();
}