Skip to content

Promises and Packaged Tasks

Promises

In some cases it might be necessary to have finer control over how the shared state is set. In these cases it might be useful to have direct control over the promises and shared tasks.

A promise allows the user to directly control how the shared state should be set. By directly controlling the future and the promise, the user has complete control over how the shared state is set. For instance, the promise value can be set directly inline, returning a future whose value is immediately ready.

promise<int> p1;
cfuture<int> f1 = p1.get_future();
p1.set_value(2);
assert(f1.get() == 2);

In fact, this is the pattern behind functions such as make_ready_future, which allows us to generate constant values as futures, so that they can interoperate with other value objects.

In practice, the promise will usually be moved into a parallel execution context where its value will be set. This becomes useful when the functionalities for launching tasks such as async and schedule do not offer enough control over the process to set the promise value. With a promise, it is possible directly control or even bypass executors. For instance, a thread might be used to set the value of the shared state.

promise<int> p2;
cfuture<int> f2 = p2.get_future();
std::thread t2([&p2]() { p2.set_value(2); });
assert(f2.get() == 2);
t2.join();

This would be not directly possible with async because std::thread is not executor. This pattern can be replicated with other types, such as boost::fiber.

This is equivalent to a more permanent solution which would be to define a custom executor that always launches tasks in new threads.

auto f3 = async(make_new_thread_executor(), []() { return 2; });
assert(f3.get() == 2);

This is how promises are related to the shared state:

graph LR M[[User code 1]] --> |store|F[Future Value] N[[User code 2]] --> |set|P[Value Promise] subgraph Futures and Promises F --> |read|S[(Shared State)] P --> |write|S end M --> |store|P M -.-> N[[User code 2]]

With a promise another library could be used to make a web request or run a process whose results will only be available in the future.

By calling executor functions directly, we can also achieve a pattern that is very similar to async:

promise<int> p4;
cfuture<int> f4 = p4.get_future();
futures::thread_pool pool(1);
pool.get_executor().execute([&p4]() { p4.set_value(2); });
assert(f4.get() == 2);

It's useful to note that promises allows us to define the functionalities of the future type via future_options. This defines the type returned by get_future.

promise<int, future_options<>> p5;
vfuture<int> f5 = p5.get_future();
std::thread t5([&p5]() { p5.set_value(2); });
assert(f5.get() == 2);
t5.join();

In this example, we explicitly define the promise should return a future with empty options. That is, the future has no associated executor and does not support continuations. This is useful for immediately available futures because any continuation to this future would not be required to poll for its results with basic_future::wait.

Packaged Tasks

The most common way to use promises is to wrap them in tasks that set their value. This pattern is simplified through a packaged_task, which stores a reference to the shared state and the task used to set its value.

graph LR M[[User code 1]] --> |store|F[Future Value] N[[User code 2]] --> |invoke|P[Packaged Task] subgraph Futures and Promises F --> |read|S[(Shared State)] P --> |write|S end M --> |store|P M -.-> N[[User code 2]]

The packaged task is a callable object that sets the shared state when invoked.

packaged_task<int()> p1([]() { return 2; });
auto f1 = p1.get_future();
p1();
assert(f1.get() == 2);

In this example, we immediately set the future value inline by invoking the packaged task. Instead of returning the value of the task packaged_task stores, any value returned by the internal task is set as the future shared state.

When we pass a packaged_task as a parameter it acts like any other callable, which makes it more convenient than promises for APIs that require callables such as when we create a std::thread:

packaged_task<int()> p2([]() { return 2; });
auto f2 = p2.get_future();
std::thread t(std::move(p2));
assert(f2.get() == 2);
t.join();

By providing a packaged_task directly to an executor, we have a pattern that is very similar to futures::async:

packaged_task<int()> p3([]() { return 2; });
auto f3 = p3.get_future();
futures::thread_pool pool(1);
pool.get_executor().execute(std::move(p3));
assert(f3.get() == 2);

Like promises, a packaged_task allows us to define the functionalities of the future type via future_options. This defines the type to be returned later by get_future .