Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is std::promise?

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?

like image 503
Kerrek SB Avatar asked Jun 12 '12 20:06

Kerrek SB


People also ask

What is a promise in C++?

A promise is an object that can store a value of type T to be retrieved by a future object (possibly in another thread), offering a synchronization point. On construction, promise objects are associated to a new shared state on which they can store either a value of type T or an exception derived from std::exception .

Is STD promise copyable?

std::promise is non-copyable, so there's no way to stick a functor with this member into a std::function .

What is an std :: future?

The class template std::future provides a mechanism to access the result of asynchronous operations: An asynchronous operation (created via std::async, std::packaged_task, or std::promise) can provide a std::future object to the creator of that asynchronous operation.

What is a future C++?

A future is an object that can retrieve a value from some provider object or function, properly synchronizing this access if in different threads. "Valid" futures are future objects associated to a shared state, and are constructed by calling one of the following functions: async.


2 Answers

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(); } 
like image 183
Kerrek SB Avatar answered Oct 04 '22 12:10

Kerrek SB


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.

like image 29
Jonathan Wakely Avatar answered Oct 04 '22 11:10

Jonathan Wakely