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 function call, while a thread is an execution context. Threads are useful in their own right, but for the purpose 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 value can be retrieved via the member function get()
, which effectively synchronizes the program by waiting 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 replacement for ordinary return types. For our example function, we expect a std::future<int>
.
Now, on to the hierarchy, from highest to lowest level:
-
std::async
: The most convenient and straight-forward way to perform an asynchronous computation is via theasync
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 executed concurrently, serially upon
get()
, or by some other black magic. However, the result is easily obtained when needed:auto res = fut.get(); // is an int
-
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 thestd::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 callable, 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 separate 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 havejoin
it at the end of the scope, or whenever (e.g. using Anthony Williams’sscoped_thread
wrapper, which really should be in the standard library). The details of usingstd::thread
don’t concern us here, though; just be sure to join or detachthr
eventually. What matters is that whenever the function call finishes, our result is ready:auto res = fut.get(); // as before
-
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 viaset_exception()
before its lifetime ends if its future is to be consumed. A satisfied promise can die without consequence, andget()
becomes available on the future. A promise with an exception will raise the stored exception upon call ofget()
on the future. If the promise dies with neither value nor exception, callingget()
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() << "https://stackoverflow.com/" << 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();
}