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.
In the
ReadAndWriteAsync
example, why does the secondreturn
usetask_from_result
and the fourth one doesn’t?In general, a continuation is allowed to return either
task
or a value directly. However, the innerthen()
has two branches, one which completes sync and the other which dispatches to another async method. Since the async branch returns atask
, the sync branch must do so as well — there’s only one return type allowed for a lambda.Pingback: The wait is over: coroutines in C++ | WriteAsync .NET
Pingback: A real async GetFiles? – WriteAsync .NET