456 votes

Qu'est-ce que std::promise ?

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

Cependant, je n'arrive pas à saisir 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 en dehors de son synopsis de classe, et il en va de même pour le document de l'UE. std::thread .

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

1 votes

(Il y a d'autres choses que je ne comprends pas : std::atomic_future y std::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 .. ?

597voto

Kerrek SB Points 194696

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 :

  1. 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
  2. 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
  3. 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();
}

0 votes

Vous avez dit "...qui synchronise effectivement le programme en attendant le résultat." . Que signifie "synchronise" ici ? Que signifie l'ensemble de la déclaration ? Je n'arrive pas à comprendre. Aucune des significations de "synchronise" de cette entrée du dictionnaire m'aide à comprendre la phrase. Est-ce que "attente" signifie "synchronisation" ? Est-ce que toute attente est synchrone ? Je pense que je comprends partiellement ce que vous voulez dire, mais je ne suis pas sûr de ce que vous en fait moyen.

0 votes

@Nawaz : "synchroniser" dans le sens de sérialiser l'exécution dans la machine abstraite... est-ce que ça a un sens ?

0 votes

@KerrekSB : Pouvez-vous s'il vous plaît élaborer un peu plus sur ce sujet ?

213voto

Jonathan Wakely Points 45593

Pour reprendre les termes de [futures.state], un std::future est un objet de retour asynchrone (" un objet qui lit les résultats à partir d'un état partagé ") et une 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 que vous set un résultat sur, afin que vous puissiez Obtenga de l'avenir associé.

Le fournisseur asynchrone est celui 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 une autre, et le détail interne de std::async est un autre. Chacune d'entre elles peut créer un état partagé et vous donner un std::future qui partage cet état, et peut rendre l'état prêt.

std::async est un utilitaire de plus haut niveau qui vous donne un objet de résultat asynchrone et se charge en interne de créer le fournisseur asynchrone et de rendre l'état partagé prêt lorsque la tâche se termine. Vous pouvez l'émuler avec un std::packaged_task (ou std::bind y un std::promise ) et un std::thread mais c'est plus sûr et plus facile à utiliser. std::async .

std::promise est d'un niveau un peu plus bas, pour les cas où vous souhaitez transmettre un résultat asynchrone au futur, mais le code qui rend le résultat prêt ne peut pas être regroupé dans une seule fonction adaptée au passage à la fonction std::async . Par exemple, vous pouvez avoir un tableau de plusieurs promise et les future et avoir un seul thread qui effectue plusieurs calculs et fixe un résultat sur chaque promesse. async ne vous permet de renvoyer qu'un seul résultat, pour en renvoyer plusieurs, vous devrez appeler async plusieurs fois, ce qui peut entraîner un gaspillage de ressources.

10 votes

Peut-on gaspiller des ressources ? Peut être incorrect, si ce code ne peut pas être parallélisé.

0 votes

"retour asynchrone" et "lit le résultat de l'état partagé" sont pour la plupart orthogonaux, ce qui rend la première phrase un peu confuse. Voulez-vous dire que le partage de l'état se fait entre le futur et la promesse ? Si c'est le cas, veuillez le dire explicitement dès le début.

0 votes

@einpoklum pourquoi avez-vous arrêté de lire "asynchronous return object" avant le dernier mot ? Je cite la terminologie de la norme. A future est un exemple concret d'une objet de retour asynchrone qui est un objet qui lit un résultat retourné de manière asynchrone, via l'état partagé. A promise est un exemple concret d'une fournisseur asynchrone qui est un objet qui écrit une valeur dans l'état partagé, qui peut être lu de manière asynchrone. Je voulais dire ce que j'ai écrit.

40voto

Paul Rubel Points 13132

Bartosz Milewski fournit un bon compte-rendu.

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

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

Une promesse est un véhicule pour passer la valeur de retour (ou un exception) du fil d'exécution d'une fonction au fil d'exécution qui qui encaisse le futur de la fonction.

...

Un futur est l'objet de synchronisation construit autour de la fonction l'extrémité réceptrice du canal de la promesse.

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

Voici un exemple tiré de cette page :

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

4 votes

C'est en voyant la promesse dans le constructeur du fil que j'ai compris. L'article de Bartosz n'est peut-être pas le meilleur, mais il explique comment les éléments se rejoignent. Merci.

33voto

Dans une approximation grossière, on peut considérer std::promise comme l'autre extrémité d'un std::future (c'est falso mais pour l'illustration, vous pouvez faire comme si c'était le cas). L'extrémité consommateur du canal de communication utiliserait un std::future pour consommer la donnée à partir de l'état partagé, tandis que le thread producteur utiliserait un fichier de type std::promise pour écrire dans l'état partagé.

0 votes

OK, mais comment je peux mettre en place quelque chose comme ça, et en quoi c'est différent de std::async ?

12 votes

@KerrekSB : std::async peut conceptuellement (ce n'est pas imposé par la norme) être compris comme une fonction qui crée une std::promise le pousse dans un pool de threads (en quelque sorte, il peut s'agir d'un pool de threads, d'un nouveau thread, ...) et renvoie l'image associée de l'utilisateur. std::future à l'appelant. Du côté client, vous attendez que le std::future et un thread à l'autre bout calculera le résultat et le stockera dans le fichier d'échange de données. std::promise . Remarque : la norme exige que le état partagé y el std::future mais pas l'existence d'un std::promise dans ce cas d'utilisation particulier.

0 votes

Je crois que j'ai enfin compris. Je vais poster du code pour référence future (sans jeu de mots).

15voto

kjp Points 1596

std::promise est le canal ou la voie d'accès aux informations à renvoyer par la fonction asynchrone. std::future est le mécanisme de synchronisation qui fait attendre l'appelant jusqu'à ce que la valeur de retour transportée dans la fonction std::promise est prêt (ce qui signifie que sa valeur est définie dans la fonction).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X