This is part two in a series of articles on asynchronous coding in C++/CX. See the introduction here.
- Prefer a task chain to nested tasks
- Be aware of thread scheduling rules
- Be 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
2. Be Aware of Thread Scheduling Rules
- When working with non-threadsafe code, you will usually prefer to continue on the same thread and not have to deal with multiple threads contending for the same data.
- Helpfully, Universal Windows Platform (UWP) apps have a user interface thread inside a Single Threaded Apartment. On that thread, continuations (either a task .then() statement or code after a co_await statement) will almost always be scheduled to continue on the same thread.
- Exception #1: if the initial task in a chain is not an IAsyncAction or IAsyncOperation (or their WithProgress variants), then the continuation may “break out” of the apartment to a different thread.
- Exception #2: in my tests, I found that mixing coroutines and task-based continuations could still break out of the apartment. Calling a coroutine and then following it with a .then() task-based continuation seemed to cause it, although I haven’t dug very deep into the exact conditions.
- If a continuation is not on the UI thread, then the guarantees go out the window. For threadpool threads, a continuation can be scheduled on any thread.
- So, partway through a coroutine, you may find yourself on an entirely different thread.
- I imagine this is to prevent “starvation” of asynchronous chains when running on a compute-intensive thread, for example. But it can be quite a gotcha.
- The concurrency::create_task() method can take a task_continuation_context::use_current() parameter to (try) to force execution to stay on the same thread, but there’s no equivalent for coroutines. It’s not clear to me that this always works, however; I need to test further and explore.
- When in doubt: use std::this_thread::get_id() to check the thread id over the course of an asynchronous chain of operation.
- When on a threadpool thread, my preference is to write a standalone coroutine in a functional style rather than as a traditional method within a class. I avoid using the this pointer, pass inputs to the coroutine by value and return a result from the asynchronous chain. If I want to modify an object, I’ll write an accompanying method that calls the coroutine, then schedules a final task on the original thread to mutate the object. Here’s an example:
class MyClass { private: static concurrency::task<std::wstring> Coroutine(wstring filename) { // ... do some stuff, open file with co_await, read data, etc... co_return result; } public: void MemberFnAsync() { concurrency::create_task(Coroutine(m_filename), task_continuation_context::use_current()) .then([this](wstring result) { this->SetTextFromFile(result); }); } };
3. Be Aware of Object and Parameter Lifetimes
- When passing data into asynchronous code, you want to pass by value. That applies to both lambda captures (for continuation tasks) and to coroutine parameters.
- Why? If you pass do a traditional C++ calling convention like this:
concurrency::task<void> MyCoroutineAsync (const wstring &str) { // do some stuff. co_await OtherFn(); AnotherFn(str); }
… the str parameter will typically be valid until the first co_await statement, but at that point execution will return to the caller until OtherFn() completes. The str parameter may well then be destroyed—especially if it was an rvalue or a stack variable. By the time the coroutine resumes and AnotherFn() is called, str will be a dangling pointer.
- The underlying issue is that the coroutine task chain outlives its caller and therefore can’t count on references to data from its caller.
- This applies to reference and pointer data. Pass-by-value is usually preferred, but of course smart pointer types are also valid.
- Just be sure to pass smart pointers in a manner that creates a separate reference count: std::shared_ptr<T>, not const std::shared_ptr<T>&
One thought on “Asynchronous Best Practices in C++/CX (Part 2)”