What is std::promise?

103,424

Solution 1

In the words of [futures.state] a std::future is an asynchronous return object ("an object that reads results from a shared state") and a std::promise is an asynchronous provider ("an object that provides a result to a shared state") i.e. a promise is the thing that you set a result on, so that you can get it from the associated future.

The asynchronous provider is what initially creates the shared state that a future refers to. std::promise is one type of asynchronous provider, std::packaged_task is another, and the internal detail of std::async is another. Each of those can create a shared state and give you a std::future that shares that state, and can make the state ready.

std::async is a higher-level convenience utility that gives you an asynchronous result object and internally takes care of creating the asynchronous provider and making the shared state ready when the task completes. You could emulate it with a std::packaged_task (or std::bind and a std::promise) and a std::thread but it's safer and easier to use std::async.

std::promise is a bit lower-level, for when you want to pass an asynchronous result to the future, but the code that makes the result ready cannot be wrapped up in a single function suitable for passing to std::async. For example, you might have an array of several promises and associated futures and have a single thread which does several calculations and sets a result on each promise. async would only allow you to return a single result, to return several you would need to call async several times, which might waste resources.

Solution 2

I understand the situation a bit better now (in no small amount due to the answers here!), so I thought I add a little write-up of my own.


There are two distinct, though related, concepts in C++11: Asynchronous computation (a function that is called somewhere else), and concurrent execution (a thread, something that does work concurrently). The two are somewhat orthogonal concepts. Asynchronous computation is just a different flavour of func­tion call, while a thread is an execution context. Threads are useful in their own right, but for the pur­pose of this discussion, I will treat them as an implementation detail.


There is a hierarchy of abstraction for asynchronous computation. For example's sake, suppose we have a function that takes some arguments:

int foo(double, char, bool);

First off, we have the template std::future<T>, which represents a future value of type T. The val­ue can be retrieved via the member function get(), which effectively synchronizes the program by wait­ing for the result. Alternatively, a future supports wait_for(), which can be used to probe whether or not the result is already available. Futures should be thought of as the asynchronous drop-in re­place­ment for ordinary return types. For our example function, we expect a std::future<int>.

Now, on to the hierarchy, from highest to lowest level:

  1. std::async: The most convenient and straight-forward way to perform an asynchronous com­pu­ta­tion is via the async function template, which returns the matching future immediately:

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

    We have very little control over the details. In particular, we don't even know if the function is exe­cu­ted concurrently, serially upon get(), or by some other black magic. However, the result is easily ob­tained when needed:

    auto res = fut.get();  // is an int
    
  2. We can now consider how to implement something like async, but in a fashion that we control. For example, we may insist that the function be executed in a separate thread. We already know that we can provide a separate thread by means of the std::thread class.

    The next lower level of abstraction does exactly that: std::packaged_task. This is a template that wraps a function and provides a future for the functions return value, but the object itself is call­able, and calling it is at the user's discretion. We can set it up like this:

    std::packaged_task<int(double, char, bool)> tsk(foo);
    
    auto fut = tsk.get_future();    // is a std::future<int>
    

    The future becomes ready once we call the task and the call completes. This is the ideal job for a se­pa­rate thread. We just have to make sure to move the task into the thread:

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

    The thread starts running immediately. We can either detach it, or have join it at the end of the scope, or whenever (e.g. using Anthony Williams's scoped_thread wrapper, which really should be in the standard library). The details of using std::thread don't concern us here, though; just be sure to join or detach thr eventually. What matters is that whenever the function call finishes, our result is ready:

    auto res = fut.get();  // as before
    
  3. Now we're down to the lowest level: How would we implement the packaged task? This is where the std::promise comes in. The promise is the building block for communicating with a future. The principal steps are these:

    • The calling thread makes a promise.

    • The calling thread obtains a future from the promise.

    • The promise, along with function arguments, are moved into a separate thread.

    • The new thread executes the function and fulfills the promise.

    • The original thread retrieves the result.

    As an example, here's our very own "packaged task":

    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
    };
    

    Usage of this template is essentially the same as that of std::packaged_task. Note that moving the entire task subsumes moving the promise. In more ad-hoc situations, one could also move a promise object explicitly into the new thread and make it a function argument of the thread function, but a task wrapper like the one above seems like a more flexible and less intrusive solution.


Making exceptions

Promises are intimately related to exceptions. The interface of a promise alone is not enough to convey its state completely, so exceptions are thrown whenever an operation on a promise does not make sense. All exceptions are of type std::future_error, which derives from std::logic_error. First off, a description of some constraints:

  • A default-constructed promise is inactive. Inactive promises can die without consequence.

  • A promise becomes active when a future is obtained via get_future(). However, only one future may be obtained!

  • A promise must either be satisfied via set_value() or have an exception set via set_exception() before its lifetime ends if its future is to be consumed. A satisfied promise can die without consequence, and get() becomes available on the future. A promise with an exception will raise the stored exception upon call of get() on the future. If the promise dies with neither value nor exception, calling get() on the future will raise a "broken promise" exception.

Here is a little test series to demonstrate these various exceptional behaviours. First, the harness:

#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;
    }
}

Now on to the tests.

Case 1: Inactive promise

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

Case 2: Active promise, unused

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

Case 3: Too many futures

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

Case 4: Satisfied promise

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".

Case 5: Too much 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();
}

The same exception is thrown if there is more than one of either of set_value or set_exception.

Case 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

Case 7: Broken promise

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
    }   // Error: "broken promise"

    return fut.get();
}

Solution 3

Bartosz Milewski provides a good writeup.

C++ splits the implementation of futures into a set of small blocks

std::promise is one of these parts.

A promise is a vehicle for passing the return value (or an exception) from the thread executing a function to the thread that cashes in on the function future.

...

A future is the synchronization object constructed around the receiving end of the promise channel.

So, if you want to use a future, you end up with a promise that you use to get the result of the asynchronous processing.

An example from the page is:

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

Solution 4

In a rough approximation you can consider std::promise as the other end of a std::future (this is false, but for illustration you can think as if it was). The consumer end of the communication channel would use a std::future to consume the datum from the shared state, while the producer thread would use a std::promise to write to the shared state.

Solution 5

std::promise is the channel or pathway for information to be returned from the async function. std::future is the synchronization mechanism thats makes the caller wait until the return value carried in the std::promise is ready(meaning its value is set inside the function).

Share:
103,424
Kerrek SB
Author by

Kerrek SB

Updated on July 08, 2022

Comments

  • Kerrek SB
    Kerrek SB almost 2 years

    I'm fairly familiar with C++11's std::thread, std::async and std::future components (e.g. see this answer), which are straight-forward.

    However, I cannot quite grasp what std::promise is, what it does and in which situations it is best used. The standard document itself doesn't contain a whole lot of information beyond its class synopsis, and neither does std::thread.

    Could someone please give a brief, succinct example of a situation where an std::promise is needed and where it is the most idiomatic solution?

  • Kerrek SB
    Kerrek SB almost 12 years
    OK, but how would I set something like that up, and how is this different from std::async?
  • David Rodríguez - dribeas
    David Rodríguez - dribeas almost 12 years
    @KerrekSB: std::async can conceptually (this is not mandated by the standard) understood as a function that creates a std::promise, pushes that into a thread pool (of sorts, might be a thread pool, might be a new thread, ...) and returns the associated std::future to the caller. On the client side you would wait on the std::future and a thread on the other end would compute the result and store it in the std::promise. Note: the standard requires the shared state and the std::future but not the existence of a std::promise in this particular use case.
  • Kerrek SB
    Kerrek SB almost 12 years
    Seeing the promise in the thread's constructor finally made the penny drop. Bartosz's article is maybe not the greatest, but it does explain how the elements tie together. Thanks.
  • Kerrek SB
    Kerrek SB almost 12 years
    I think I finally got it. I'll post some code for future reference (no pun intended).
  • Kerrek SB
    Kerrek SB almost 12 years
    OK, so can I implement an std:async(func, myargs...) with a thread? Like so: std::promise<R> pr; std::future<R> ft = pr.get_future(); std::thread t([](std::promise<R> p, Args ...args) { p.set_value(func(args...)); }, std::move(pr), myargs...);? Who joins the thread? How does the future's get() know that it has to call join on the thread?
  • David Rodríguez - dribeas
    David Rodríguez - dribeas almost 12 years
    @KerrekSB: std::future will not call join on the thread, it has a pointer to a shared state which is the actual communication buffer. The shared state has a synchronization mechanism (probably std::function + std::condition_variable to lock the caller until the std::promise is fulfilled. The execution of the thread is orthogonal to all this, and in many implementations you might find that std::async are not executed by new threads that are then joined, but rather by a thread pool whose lifetime extends until the end of the program.
  • Marc Mutz - mmutz
    Marc Mutz - mmutz almost 12 years
    @DavidRodríguez-dribeas: please edit the information from the comments into your answer.
  • Jonathan Wakely
    Jonathan Wakely almost 12 years
    @David, std::async(std::launch::async, ...) must behave as if executed in a new thread, not a thread pool. See [futures.async], "as if in a new thread of execution represented by a thread object [...] a call to a waiting function on an asynchronous return object that shares the shared state created by this async call shall block until the associated thread has completed, as if joined" (emphasis mine)
  • David Rodríguez - dribeas
    David Rodríguez - dribeas almost 12 years
    @JonathanWakely: That does not mean that it has to be executed in a new thread, only that it has to be executed asynchronously as-if it was run in a newly created thread. The main advantage of std::async is that the runtime library can make the right decisions for you with respect to the number of threads to create and in most cases I will expect runtimes that use thread pools. Currently VS2012 does use a thread pool under the hood, and it does not violate the as-if rule. Note that there are very little guarantees that need to be fulfilled for this particular as-if.
  • Jonathan Wakely
    Jonathan Wakely almost 12 years
    Thread locals need to be re-initialized, but the as-if rule allows anything (which is why I put "as if" in italics :)
  • Puppy
    Puppy over 11 years
    Might waste resources? Might be incorrect, if that code cannot be parallelized.
  • Nawaz
    Nawaz about 11 years
    You said "...which effectively synchronizes the program by wait­ing for the result.". What does "synchronizes" mean here? What does the entire statement mean? I'm unable to understand this. None of the meaning of "synchronize" from this dictionary entry helps me understand the sentence. Does just "waiting" mean "synchronization"? Does every wait synchronize? I think I partially understand what you mean, but I'm not sure what you actually mean.
  • Kerrek SB
    Kerrek SB about 11 years
    @Nawaz: "synchronizing" in the sense of serializing execution in the abstract machine... does that make sense?
  • Nawaz
    Nawaz about 11 years
    @KerrekSB: Can you please elaborate on it a bit more?
  • Kerrek SB
    Kerrek SB about 11 years
    @Nawaz: It's about how you model concurrent execution mentally. You think of executing all the statements in some interleaved sequence, but any serialization is valid -- except when you have a synchronisation point, which enforces constraints on the serialization. The standard actually defines the concept of "synchronizes-with" in this regard. I believe that's the sense which I had in mind (although it's been a while and I'd have to re-read what I wrote!).
  • Nawaz
    Nawaz about 11 years
    @KerrekSB: Thanks. I was looking for the phrase "synchronizes-with". Now that makes sense to me. std::future<T>::get() actually synchronizes-with the current thread and to do that it may has to wait . That is how I understand asynchronous computation and synchronization (with the current thread).
  • Nawaz
    Nawaz about 11 years
    @KerrekSB: So I think "...which effectively synchronizes the asynchronous call with the current thread by (optionally) wait­ing for the result." makes more sense, as it is precisely worded. BTW, this answer deserves a blog. Do you blog?
  • Kerrek SB
    Kerrek SB about 11 years
    @Nawaz: Yeah, that would be more precise, but I thought my shorter statement would make sense in context... of course it doesn't synchronise everything :-) I don't usually, blog I'm afraid. Maybe I could guest-write for RMF's blog?
  • StereoMatching
    StereoMatching over 10 years
    Nice answer, thanks for your help.About the part of std::async, I remember that we could determine it would spawn another thread or work synchronous with flag(std::launch::async, std::launch::deferred)
  • Kerrek SB
    Kerrek SB over 10 years
    @StereoMatching: Yes, those flags exist, though I'm not sure right now if they're binding of if they're only a hint. I believe a platform without threading support could still offer async and just perform the operation serially, though I'd have to check whether the launch flags make any guarantees.
  • Felix Dombek
    Felix Dombek over 10 years
    In my_task, are you sure that explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { } is correct? Wouldn't the constructor just take a function (pointer) and initialize the internal std::function with it? Why are you forwarding multiple arguments?
  • Kerrek SB
    Kerrek SB over 10 years
    @FelixDombek: Perfect forwarding etc. std::function has many constructors; no reason not to expose those to the consumer of my_task.
  • Rapptz
    Rapptz over 9 years
    @KerrekSB They're binding. Anything other than the two flags (std::launch::async and std::launch::deferred, along with not providing a policy at all) are implementation defined and non-portable.
  • Daveed V.
    Daveed V. about 9 years
    @KerrekSB: Thanks for the write-up! A nit: << A promise with an exception will raise the stored exception upon call of get() on the future, while a promise with neither value nor exception will raise a "broken promise" exception. >> That makes it sound as if if calling get() on a future whose associated promise hasn't been satisfied yet would raise the broken_promise exception. I think it usually will just block until (hopefully) the promise is satisfied. If the promise is destroyed, it "abandons" the state shared with the future, which records the broken_promise exception.
  • Kerrek SB
    Kerrek SB about 9 years
    @DaveedV.: Thanks for the feedback! Yes, that is test case 7: If you destroy the promise without setting either value or exception, then calling get() on the future raises an exception. I will clarify this by adding "before it is destroyed"; please let me know if that's sufficiently clear.
  • sunny moon
    sunny moon about 7 years
    Finally got() my future of grokking the thread support library on the promise of your amazing explanation!
  • Mudit Jain
    Mudit Jain over 6 years
    Wouldn't case 2 result in broken promise exception since pr was destroyed after return of test() similar to case 7?
  • einpoklum
    einpoklum about 5 years
    "asynchronous return" and "reads result from shared state" are mostly orthogonal, which makes the first sentence a bit confusing. Do you mean to say that the sharing of the state is between the future and the promise? If so, please say that explicitly from the outset.
  • Jonathan Wakely
    Jonathan Wakely about 5 years
    @einpoklum why did you stop reading "asynchronous return object" before the last word? I'm quoting the terminology of the standard. A future is a concrete example of an asynchronous return object, which is an object that reads a result that was returned asynchronously, via the shared state. A promise is a concrete example of an asynchronous provider, which is an object that writes a value to the shared state, which can be read asynchronously. I meant what I wrote.
  • madhur4127
    madhur4127 over 2 years
    "using Anthony Williams's scoped_thread wrapper, which really should be in the standard library" this is wrong as of C++20, there's std::jthread