Async in C++ with the PPL

Spread the love

The Parallel Patterns Library (PPL) provides C++ developers with some very useful concurrency primitives along similar lines as Task parallelism in .NET. Having been introduced in Visual Studio 2010, the PPL is not new by any means. However, it remains relatively obscure to many who dabble in native code.

Similar to .NET, task<T> is the main star of the native async show. In contrast to C#, C++ allows the use of void as a template parameter, so you can pass around task<void> instances.

// MyAsyncWorker.h
#include <ppltasks.h>

class MyAsyncWorker
{
public:
// ...
    concurrency::task<void> DoWorkAsync();
    concurrency::task<int> DoWorkWithResultAsync();
};

// Main.cpp
#include "MyAsyncWorker.h"

using namespace concurrency;

// ...
MyAsyncWorker w;
task<void> t1 = w.DoWorkAsync();

// does not return anything, but will throw if task has failed.
t1.get();

task<int> t2 = w.DoWorkWithResultAsync();

// Returns value or throws if task has failed.
int v = t2.get();

// ...

The PPL equivalent to TaskCompletionSource is task_completion_event. The API should look reasonably familiar, though note that there are no Try... methods — instead the set... methods all return bool to indicate status. To set an exception, std::exception_ptr is used; see the MSDN article “Transporting Exceptions Between Threads” for more details.

// MyAsyncWorker.cpp
task<void> MyAsyncWorker::DoWorkAsync()
{
    task_completion_event<void> tce;

    // schedule some background work somehow...
    StartBackgroundWork(tce);

    // There is no 'get_task' method; instead create a task from the TCE instance
    return task<void>(tce);
}

// ...
void OnWorkCompleted(task_completion_event<void> tce)
{
    // Note that the set method for <void> takes no params
    tce.set();
}

void OnWorkFailed(task_completion_event<void> tce, int errorCode)
{
    // Make an exception_ptr from our custom exception instance
    tce.set_exception(make_exception_ptr(worker_error(errorCode)));
}

As far as continuations go, the PPL is about on par with .NET 4.0 — that is, you won’t get much compiler code generation help or the niceties of async/await. You’ll have to make do with then, the ContinueWith equivalent. Here is an example of a multi-step async method using an imaginary pipe/buffer API. Note the use of std::shared_ptr to manage heap objects so that they don’t go out of scope prematurely.

task<bool> ReadAndWriteAsync(Pipe & input, Pipe & output)
{
    shared_ptr<Buffer> buffer = make_shared<Buffer>();
    return input.ReadAsync(*buffer).then([buffer, &output](task<unsigned int> t1)
    {
        unsigned int bytesRead = t1.get();
        if (bytesRead == 0)
        {
            return task_from_result(false);
        }

        return output.WriteAsync(*buffer).then([](task<unsigned int> t2)
        {
            unsigned int bytesWritten = t2.get();
            return bytesWritten > 0;
        });
    });
}

There is no Task.Delay in the PPL. Instead, you need to use timer to set a task_completion_event after a certain timeout. There is a sample covering this on MSDN, but it is far easier to pick up the PPL Asynchronous Sample Pack which includes a readymade implementation (create_delayed_task) in the ppltasks_extra.h header.

#include "ppltasks_extra.h"

using namespace concurrency;
using namespace concurrency::extras;
// ...

task<int> DelayForOneMillisecondAsync()
{
    return create_delayed_task(std::chrono::milliseconds(1), []()
    {
        return 42;
    });
}

To join on a group of tasks, PPL gives you when_all and when_any which are analogous to their .NET namesakes — with a C++ twist.

vector<task<void>> tasks;
tasks.push_back(Do1Async());
tasks.push_back(Do2Async());

// Need to pass begin/end iterators
when_all(tasks.begin(), tasks.end()).get();

Cooperative cancellation is achieved with cancellation_token and cancellation_token_source which are, as expected, quite similar to their .NET counterparts.

cancellation_token_source cts;
task<void> task = LoopForeverAsync(cts.get_token());

wcout << L"Press ENTER to cancel." << endl;
wstring line;
getline(wcin, line);

// Request cancellation
cts.cancel();

// Task may complete with task_canceled exception
try
{
    task.get();
}
catch (task_canceled const &)
{
}

Perhaps the most surprising aspect to those accustomed to async in .NET is that the default task scheduler in the PPL does not support explicit synchronous continuations. Keep this in mind, especially when you are writing async unit tests — you may encounter unexpected thread switches.

Overall, the PPL is a boon to C++ programmers who want to explore the wild world of async. In an upcoming post, I will show a more complete native async sample using the PPL.

4 thoughts on “Async in C++ with the PPL

    1. Brian Rogers Post author

      In general, a continuation is allowed to return either task or a value directly. However, the inner then() has two branches, one which completes sync and the other which dispatches to another async method. Since the async branch returns a task, the sync branch must do so as well — there’s only one return type allowed for a lambda.

  1. Pingback: The wait is over: coroutines in C++ | WriteAsync .NET

  2. Pingback: A real async GetFiles? – WriteAsync .NET

Leave a Reply

Your email address will not be published. Required fields are marked *