For me, the steepest learning curves with the Universal Windows Platform (UWP) was the use of asynchronous APIs and the various libraries for dealing with them. Any operation that may take more than 50ms is now asynchronous, and in many cases you can’t even call the synchronous equivalent from Win32 or C. This includes networking operations, file I/O, picker dialogs, hardware device enumeration and more. While these APIs are pretty natural when writing C# code, in C++/CX it tends to be a pretty ugly affair. After two years of use, I now have a few “best practices” to share.
C++/CX offers two different approaches for dealing with asynchronous operations:
- task continuations using the Parallel Patterns Library (PPL)
- coroutines (as of early 2016)
Personally, I vastly prefer coroutines; having co_await
gives C++/CX a distinctly C# flavour, and the entire API starts to feel “natural.” However, at my current job we have not yet standardized on coroutines, and have a mix of both approaches instead. And to be fair – despite Microsoft’s assurances that they are “production ready”, I’ve personally hit a few coroutine bugs and they do occasionally completely break with compiler updates.
I’m going to write up my advice in a series of posts, as the examples can be pretty lengthy.
- Prefer a task chain to nested tasks
- Be aware of thread scheduling rules
- Stay aware of object and parameter lifetimes
- Consider the effect of OS suspend/resume
- Style: never put a try/catch block around a task chain.
- References
1. Prefer a Task Chain to Nested Tasks
When writing a series of API calls that need local variables, conditional logic, or loops, it’s tempting to write it as a nest of tasks. But a nest will:
- hurts legibility: an extra indent level for every sequential API call, and very wordy
- painful/missing exception handling: every level of the nest needs its own exception handler. Exceptions will not propagate automatically from an inner task to the outer exception handler, but will instead raise an unobserved exception error.
- makes it hard to return a waitable/gettable task that can track when the entire chain completes.
Consider this example nested code:
concurrency::create_task(folder->GetFileAsync()) .then([](StorageFile ^file) { if(file != nullptr) { concurrency::create_task(file->ReadAsync()) .then([](IRandomAccessStream ^stream) { /* do something with stream */ }) .then([](concurrency::task<void> t) { try { t.get(); } catch (COMException ^) { /* do something */} catch (...) { /* do something */ } }); } }) .then([](concurrency::task<void> t) { try { t.get(); } catch (COMException ^) { /* do something */} catch (...) { /* do something */ } });
Here’s the equivalent implemented as a task chain:
concurrency::create_task(folder->GetFileAsync()) .then([](StorageFile ^file) { if(file == nullptr) concurrency::cancel_current_task(); return file->ReadAsync(); }).then([](IRandomAccessStream ^stream) { /* do something with stream */ }).then([](concurrency::task<void> t) { try { t.get(); } catch (std::task_cancelled ) { } catch (COMException ^) { /* do something */} catch (...) { /* do something */ } });
Let’s look at a few other situations and the best way to make a chain:
- Need access to local variable in earlier stage of chain: the best solution is to define a custom struct with the local variables that are needed throughout the chain, heap-allocate it with a std::sharedptr, and pass it in via lambda-capture:
struct StackFrame { String ^filename; } auto stack = std::make_shared<StackFrame>(); concurrency::create_task(folder->GetFileAsync()) .then([stack](StorageFile ^file) { stack->filename = file->Name; return file->ReadAsync(); }).then([stack](IRandomAccessStream ^stream) { /* do something with stream and stack->filename */ })
- Need a loop with asynchronous calls on each item: collect the tasks for each item in the loop into a std::vector and return when_all on that vector.
concurrency::create_task(folder->GetFilesAsync()) .then([stack](IVector<StorageFile> ^files) { std::vector<concurrency::task<void> > tasks; for (auto file : files) { tasks.push_back(concurrency::create_task(file->DeleteAsync())); return concurrency::when_all(tasks.begin(), tasks.end()); });
Of course—just in case you’re curious—as a coroutine this is all trivial and highly readable.
try { StorageFile ^file = co_await folder->GetFileAsync(); if (file != nullptr) { IRandomAccessStream ^stream = co_await file->ReadAsync(); /* do something with stream and file->Name */ } } catch (COMException ^) { /* do something */} catch (...) { /* do something */ }
Continues in Part 2
One thought on “Asynchronous Best Practices in C++/CX (Part 1)”