PPL Task
While playing with the concurrency features in C++11, I noticed that there wasn’t any support for continuations. As a learning exercise I decided to develop something similar to tasks in The Parallel Patterns Library (PPL), where a task can run asynchronously and execute a continuation upon completion using the then member function.
1 2 3 4 5 6 |
concurrency::task<double> pplTask([testValue](){ std::this_thread::sleep_for(std::chrono::seconds(sleepDelay)); return testValue; }); auto continuation = pplTask.then([expected](const double value){ auto result = sqrt(value); Assert::AreEqual(expected, result, L"Continuation didn't produce the expected square root value for future created with a single continuation"); }); pplTask.wait(); |
A great feature of the continuation above is that the signature for the continuation depends on the return value of the task . The lambda above returns a double . Therefore if the continuation doesn’t take a single double or a task<double> as a parameter the compiler will emit an error.
Future
Firstly in order to execute some code asynchronously I decided to use std::async which generates a std::future to hold the result of the asynchronous operation. In order to be able to support multiple continuations I store a std::shared_future rather than a std::future allowing a get() call for each continuation. If I didn’t do this an exception would be fired by attempting to call get() multiple times on the future. These implementation details don’t have any impact on the interface of the FutureWithContinuations class however. The class constructor has two template parameters. One defines the callable and the other defines a parameter pack, allowing me to specify any number of arguments to pass through to the callable.
1 2 3 4 5 6 7 8 9 |
template<typename Function, typename... ArgTypes> FutureWithContinuations(Function&& futureFunction, ArgTypes&&... args) : m_continuations() , m_future() { // Let the concurrency runtime decide whether to run asynchronously or to defer running of the future auto future = std::async(std::launch::any, std::forward<Function>(futureFunction), std::forward<ArgTypes>(args)...); m_future = future.share(); } |
Continuation
I toyed with implementing the continuations using std::bind to bind the callable to a parameter pack. This would allow me to specify any number of arguments to pass to the continuation. The first argument was going to be the return type of the std::future , which is why I used std::placeholders::_1 in the initialiser list for the continuation.
1 2 3 4 5 6 7 |
using ContinuationSignature = std::function < void(ArgTypes...) > ; using bind_type = decltype(std::bind(std::declval<ContinuationSignature>(), std::declval<ArgTypes>()...)); template<typename ...ConstructorArgTypes> Continuation(ContinuationSignature continuationFunction, ConstructorArgTypes&&... args) : IContinuation() , m_boundFunction(continuationFunction, std::placeholders::_1, std::forward<ConstructorArgTypes>(args)...){} |
However this seemed a bit clunky just to support passing multiple arguments to the continuation. In most cases the only argument that is of any interest for a continuation is the return value of the future. I also read the chapter in Scott Meyer’s Effect Modern C++ book recommending the use of lambdas over std::bind . The main arguments for using lambdas include better readability and potentially faster code. Therefore I opted against using std::bind and a parameter pack.
The next hurdle to overcome was the fact that the asynchronous operation might not return anything. Therefore the continuation shouldn’t expect a parameter to be passed to it. In order to achieve this I employed template specialisation and SFINAE. The base version of the Continuation class defaults the third template parameter to void.
1 2 |
template<typename FutureReturnType, typename ContinuationFunction, typename SFINAE = void> class Continuation final : public IContinuation{...} |
whereas the specialised version uses the type if the future return type isn’t void. I do this by calling std::enable_if and std::is_void to check the future’s return type.
1 2 |
template<typename FutureReturnType, typename ContinuationFunction> class Continuation<FutureReturnType, ContinuationFunction, typename std::enable_if<!std::is_void<FutureReturnType>::value>::type> final : public IContinuation{...} |
Note that this typename can be simplified even further in C++14 by writing std::enable_if_t<!std::is_void_v<FutureReturnType>> . The only difference between the two classes is that the execute function for the continuation, with a non-void future return type, passes the shared future as a parameter to the continuation function.
1 2 3 4 |
void execute() override { m_continuationFn(m_sharedFuture.get()); } |
Summary
In order to validate my code I posted it on StackExchange’s code review site. The only review I got was from a reviewer that didn’t fully understand the code, but at least they took the time to post their comments. I also wrote tests as part of the validation. In order to simulate a long running asynchronous operation to precede the continuation I used std::this_thread::sleep_for(std::chrono::seconds(sleepDelay)); . The sleep delay was set to two seconds.
Source Code
The source code can be found on Bitbucket.