Coroutines#
pw_async2: Cooperative async tasks for embedded
For projects using C++20, pw_async2 provides first-class support for
coroutines via Coro. This allows writing asynchronous
logic in a sequential, synchronous style, eliminating the need to write explicit
state machines. The co_await keyword is used to suspend execution until an
asynchronous operation is Ready.
See also Pigweed Blog #5: C++20 coroutines without heap allocation, a blog post on how Pigweed implements coroutines without heap allocation, and challenges encountered along the way.
Using coroutines#
Define a coroutine#
A pw_async2 coroutine is a function that with CoroContext as its first parameter and a return type of
Coro<T>. Here is an example of a coroutine that returns
pw::Status:
1#include "pw_async2/channel.h"
2#include "pw_async2/coro.h"
3#include "pw_log/log.h"
4#include "pw_result/result.h"
5#include "pw_status/status.h"
6
7namespace {
8
9using ::pw::OkStatus;
10using ::pw::Status;
11using ::pw::async2::Coro;
12using ::pw::async2::CoroContext;
13using ::pw::async2::Receiver;
14using ::pw::async2::Sender;
15
16/// Create a coroutine which asynchronously receives a value from `receiver` and
17/// forwards it to `sender`.
18///
19/// Note: the `CoroContext` argument is used by the `Coro<T>` internals to
20/// allocate the coroutine state. If this allocation fails, the coroutine
21/// aborts.
22Coro<Status> ForwardingCoro(CoroContext,
23 Receiver<int> receiver,
24 Sender<int> sender) {
25 std::optional<int> data = co_await receiver.Receive();
26 if (!data.has_value()) {
27 PW_LOG_ERROR("Receiving failed: channel has closed");
28 co_return Status::Unavailable();
29 }
30
31 if (!(co_await sender.Send(*data))) {
32 PW_LOG_ERROR("Sending failed: channel has closed");
33 co_return Status::Unavailable();
34 }
35
36 co_return OkStatus();
37}
38
39} // namespace
Any future or coroutine can be passed to
co_await, which evaluates to a value_type when the result is ready. To
return from a coroutine, use co_return <expression> instead of the usual
return <expression> syntax.
Tip
Use PW_CO_TRY and PW_CO_TRY_ASSIGN instead of PW_TRY and
PW_TRY_ASSIGN when working with pw::Status or pw::Result in
a coroutine. These macros use co_return instead of return.
Run a coroutine#
Run a coroutine as a pw_async2 task using
Dispatcher::Post(). The following posts a coroutine as a CoroTask:
SharedPtr<CoroTask<Status>> task = dispatcher.Post<ForwardingCoro>(
allocator, std::move(receiver1_), std::move(sender2_));
// The task is automatically posted when allocated.
dispatcher.RunToCompletion();
The coroutine can also be instantiated directly and passed to Post, though
this requires listing the allocator twice:
SharedPtr<CoroTask<Status>> task = dispatcher.Post(
allocator,
ForwardingCoro(allocator, std::move(receiver1_), std::move(sender2_)));
// The task is automatically posted when allocated.
dispatcher.RunToCompletion();
The previous examples use CoroTask, which crashes if coroutione stack allocation fails. To handle allocation failures gracefully with FallibleCoroTask, pass an allocation error handler function after the coroutine:
auto task = dispatcher.Post(
allocator,
ForwardingCoro(allocator, std::move(receiver1_), std::move(sender2_)),
[] { PW_LOG_ERROR("coroutine allocation failed! Aborting..."); });
// The task is automatically posted when allocated.
dispatcher.RunToCompletion();
CoroTask or FallibleCoroTask can be stack or statically allocated instead of dynamically allocated with Dispatcher::Post(). This is not recommended, as it is more complex and does not eliminate all allocations. Coroutines always dynamically allocate their stacks.
// NOT RECOMMENDED: Manually declare a CoroTask to wrap a Coro. The coroutine
// itself still requires dynamic allocation.
pw::async2::CoroTask task(
ForwardingCoro(allocator, std::move(receiver1_), std::move(sender2_)));
dispatcher.Post(task);
dispatcher.RunToCompletion();
For a more details about Pigweed’s coroutine support, see Coro.
Memory#
When using C++20 coroutines, the compiler generates code to save the
coroutine’s state (including local variables) across suspension points
(co_await). pw_async2 hooks into this mechanism to control where this
state is stored and support gracefully handling allocation failures.
A pw_async2 coroutine must accept a CoroContext by value as its first argument. CoroContext wraps a reference to a pw::Allocator, and this
allocator is used to allocate the coroutine frame. When instantiating a
coroutine, simply pass an allocator as the first argument; CoroContext is
implicitly constructible from an Allocator&.
If allocation fails, the resulting Coro object is invalid. Coroutine
execution halts, and what happens next depends on the task executing the
coroutine. CoroTask crashes with PW_CRASH on
allocation failure. FallibleCoroTask
invokes an error handler function instead.
Passing data between coroutines#
Coroutines run within pw_async2 tasks and can pass data in all the same
ways. See Channels for details about passing data with
channels.