C++ – std::promise

cc++11multithreadingpromisestandard-library

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?

Best Answer

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