Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

future composability, and boost::wait_for_all

I just read the article 'Futures Done Right', and the main thing that c++11 promises are lacking seems to be that creating composite futures from existing ones

I'm looking right now at the documentation of boost::wait_for_any

but consider the following example:

int calculate_the_answer_to_life_the_universe_and_everything()
{
    return 42;
}

int calculate_the_answer_to_death_and_anything_in_between()
{
    return 121;
}

boost::packaged_task<int> pt(calculate_the_answer_to_life_the_universe_and_everything);
boost:: future<int> fi=pt.get_future();
boost::packaged_task<int> pt2(calculate_the_answer_to_death_and_anything_in_between);
boost:: future<int> fi2=pt2.get_future();

....


int calculate_the_oscillation_of_barzoom(boost::future<int>& a, boost::future<int>& b)
{
    boost::wait_for_all(a,b);
    return a.get() + b.get();
}

boost::packaged_task<int> pt_composite(boost::bind(calculate_the_oscillation_of_barzoom, fi , fi2));
boost:: future<int> fi_composite=pt_composite.get_future();

What is wrong with this approach to composability? is this a valid way to achieve composability? do we need some elegant syntactic edulcorant over this pattern?

like image 566
lurscher Avatar asked Jan 05 '13 09:01

lurscher


2 Answers

when_any and when_all are perfectly valid ways to compose futures. They both correspond to parallel composition, where the composite operation waits for either one or all the composed operations.

We also need sequential composition (which is not in Boost.Thread). This could be, for example, a future<T>::then function that allows you to queue up an operation that uses the future's value and runs when the future is ready. It is possible to implement this yourself, but with an efficiency tradeoff. Herb Sutter talks about this in his recent Channel9 video.

N3428 is a draft proposal for adding these features (and more) to the C++ standard library. They are all library features and don't add any new syntax to the language. Additionally, N3328 is a proposal to add syntax for resumable functions (like using async/await in C#) which will use future<T>::then internally.

like image 199
Roshan Shariff Avatar answered Sep 28 '22 15:09

Roshan Shariff


Points for the use of the word edulcorant. :)

The problem with your sample code is that you package everything up into tasks, but you never schedule those tasks for execution!

int calculate_the_answer_to_life() { ... }

int calculate_the_answer_to_death() { ... }

std::packaged_task<int()> pt(calculate_the_answer_to_life);
std::future<int> fi = pt.get_future();
std::packaged_task<int()> pt2(calculate_the_answer_to_death);
std::future<int> fi2 = pt2.get_future();

int calculate_barzoom(std::future<int>& a, std::future<int>& b)
{
    boost::wait_for_all(a, b);
    return a.get() + b.get();
}

std::packaged_task<int()> pt_composite([]{ return calculate_barzoom(fi, fi2); });
std::future<int> fi_composite = pt_composite.get_future();

If at this point I write

pt_composite();
int result = fi_composite.get();

my program will block forever. It will never complete, because pt_composite is blocked on calculate_barzoom, which is blocked on wait_for_all, which is blocked on both fi and fi2, neither of which will ever complete until somebody executes pt or pt2 respectively. And nobody will ever execute them, because my program is blocked!

You probably meant me to write something like this:

std::async(pt);
std::async(pt2);
std::async(pt_composite);
int result = fi_composite.get();

This will work. But it's extremely inefficient — we spawn three worker threads (via three calls to async), in order to perform two threads' worth of work. That third thread — the one running pt_composite — will be spawned immediately, and then just sit there asleep until pt and pt2 have finished running. That's better than spinning, but it's significantly worse than not existing: it means that our thread pool has one fewer worker than it ought to have. In a plausible thread-pool implementation with only one thread per CPU core, and a lot of tasks coming in all the time, that means that we've got one CPU core just sitting idle, because the worker thread who was meant to be running on that core is currently blocked inside wait_for_all.

What we want to do is declare our intentions declaratively:

int calculate_the_answer_to_life() { ... }

int calculate_the_answer_to_death() { ... }

std::future<int> fi = std::async(calculate_the_answer_to_life);
std::future<int> fi2 = std::async(calculate_the_answer_to_death);
std::future<int> fi_composite = std::when_all(fi, fi2).then([](auto a, auto b) {
    assert(a.is_ready() && b.is_ready());
    return a.get() + b.get();
});

int result = fi_composite.get();

and then have the library and the scheduler work together to Do The Right Thing: don't spawn any worker thread that can't immediately proceed with its task. If the end-user has to write even a single line of code that explicitly sleeps, waits, or blocks, some performance is definitely being lost.

In other words: Spawn no worker thread before its time.


Obviously it's possible to do all this in standard C++, without library support; that's how the library itself is implemented! But it's a huge pain to implement from scratch, with many subtle pitfalls; so that's why it's a good thing that library support seems to be coming soon.

The ISO proposal N3428 mentioned in Roshan Shariff's answer has been updated as N3857, and N3865 provides even more convenience functions.

like image 42
Quuxplusone Avatar answered Sep 28 '22 17:09

Quuxplusone