In asynchronous applications it is common to off-load tasks to worker threads, which send back the result to the main thread after the computation for visualizing it or to take other actions. This back-and-forth can involve multiple steps, where the result of each step can influence one or more of the subsequent ones.
Three main problems typically arise in this scenario. The code becomes hard to reason about; the more the jumps back-and-forth between main and worker threads, the more the program logic gets confused. Worker threads need to reference some objects owned by the main thread, where the result will be put, but must not access data owned by the main thread, since it may cause race conditions. When a worker terminates, it needs to put the results back in a data structure owned by the main thread, which may already be out of scope (for example if a user closes a window). Typical real-world examples are Qt programs, or Chromium. Qt tries to solve these problems using slots/signals and QPointers; others, like Chromium, use weak pointers and function callbacks.
Both approaches make the code less readable. Naive solutions require heavy usage of nested lambdas with complex captures, or technologies such as Qt signals, scattering the various parts of the program logic across the codebase. They also delegate to programmers the task of avoiding race conditions and checking the lifetimes of the objects used to collect results on the main thread.
This talk shows how to solve these problems using the upcoming C++ 20 coroutines.
In the talk, we will provide some background on the coroutine features we need for this goal. We will then show a real world use-case based on Qt, where we use coroutines to: employ a sequential syntax for writing asynchronous code which offloads processing tasks to worker threads, without fragmenting it with lambdas nor with Qt-style signals/slots; automatically migrate execution between main and worker threads, supporting asynchronous program logic of arbitrary complexity; automatically guard captured variables owned by the main thread against race conditions; automatically check at the end of a task that the storage for the result is still valid, and gracefully handle the case where they aren’t.
We believe that this pattern can be useful to other C++ programmers, especially those writing asynchronous code in GUIs.
Check out more of our talks at: