Skip to content

Shared Futures

In a shared future, multiple tasks are allowed to wait for and depend on the shared state with another promise.

graph TB F1[Shared Future] --> |read|S[(Shared State)] F2[Shared Future] --> |read|S[(Shared State)] F3[...] --> |read|S[(Shared State)] F4[Shared Future] --> |read|S[(Shared State)] P[Promise] --> |write|S

A shared future can be created from a regular future with the function basic_future::share:

cfuture<int> f1 = async([] { return 1; });
shared_cfuture<int> f2 = f1.share();

When creating a shared future, the previous future value is consumed, and it becomes invalid.

cfuture<int> f1 = async([] { return 1; });
shared_cfuture<int> f2 = f1.share();
assert(!f1.valid());
assert(f2.valid());

For this reason, it's common to create shared futures in a single step.

shared_cfuture<int> f = async([] { return 1; }).share();
assert(f.get() == 1);

The main difference between a regular future and a shared future is that many valid futures might refer to the same shared state:

shared_cfuture<int> f1 = async([] { return 1; }).share();

// OK to copy
shared_cfuture<int> f2 = f1;

While a regular future object is invalidated when its value is read, a value shared by many futures can be read multiple times.

// OK to get
assert(f1.get() == 1);

// OK to call get on the other future
assert(f2.get() == 1);

// OK to call get twice
assert(f1.get() == 1);
assert(f2.get() == 1);

However, this comes at a cost. The main consideration when using a shared future is that the value returned by the get function is not moved outside the future.

// OK to get
assert(f1.get() == 1);

// OK to call get on the other future
assert(f2.get() == 1);

// OK to call get twice
assert(f1.get() == 1);
assert(f2.get() == 1);

This means the reference returned by basic_future::get needs to be handled carefully. If its value is attributed to another object of the same time, this involves copying the value instead of moving it. For this reason, we always use unique futures as a default.

cfuture<std::vector<int>> f = async([] {
    return std::vector<int>(1000, 0);
});
std::vector<int> v = f.get(); // value is moved
assert(!f.valid());           // future is now invalid
shared_cfuture<std::vector<int>>
    f = async([] {
            return std::vector<int>(1000, 0);
        }).share();
std::vector<int> v = f.get();  // value is copied
assert(f.valid());             // future is still valid
std::vector<int> v2 = f.get(); // value is copied again

Having said that, there are applications where sharing the results from the future are simply necessary. If multiple tasks depend on the result of the previous task, there might be no alternative to shared futures. In some applications, futures might contain only trivial values that are cheap to copy. In other applications, sharing a reference or a shared pointer by value without making copies might also be acceptable.

Like std::future and std::shared_future, the classes jfuture, cfuture, jcfuture also have their shared counterparts shared_jfuture, shared_cfuture, shared_jcfuture. In fact, any future defined by basic_future has its shared counterpart where its future_options define the state as shared.