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 you to write
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.
Coro<Status> ReadAndSend(Reader& reader, Writer& writer) {
// co_await suspends the coroutine until the Read operation completes.
Result<Data> data = co_await reader.Read();
if (!data.ok()) {
co_return data.status();
}
// The coroutine resumes here and continues.
co_await writer.Write(*data);
co_return OkStatus();
}
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.
Define tasks#
The following code example demonstrates basic usage:
1#include "pw_allocator/allocator.h"
2#include "pw_async2/coro.h"
3#include "pw_log/log.h"
4#include "pw_result/result.h"
5
6namespace {
7
8using ::pw::OkStatus;
9using ::pw::Result;
10using ::pw::Status;
11using ::pw::async2::Coro;
12using ::pw::async2::CoroContext;
13
14class MyReceiver;
15class MySender;
16
17/// Create a coroutine which asynchronously receives a value from
18/// ``receiver`` and forwards it to ``sender``.
19///
20/// Note: the ``CoroContext`` argument is used by the ``Coro<T>`` internals to
21/// allocate the coroutine state. If this allocation fails, ``Coro<Status>``
22/// will return ``Status::Internal()``.
23Coro<Status> ReceiveAndSendCoro(CoroContext&,
24 MyReceiver receiver,
25 MySender sender) {
26 Result<MyData> data = co_await receiver.Receive();
27 if (!data.ok()) {
28 PW_LOG_ERROR("Receiving failed: %s", data.status().str());
29 co_return Status::Unavailable();
30 }
31 Status sent = co_await sender.Send(std::move(*data));
32 if (!sent.ok()) {
33 PW_LOG_ERROR("Sending failed: %s", sent.str());
34 co_return Status::Unavailable();
35 }
36 co_return OkStatus();
37}
38
39} // namespace
Any value with a Poll<T> Pend(Context&) method can be passed to
co_await, which will return with a T when the result is ready. The
PendFuncAwaitable class can also be
used to co_await on a provided delegate function.
To return from a coroutine, co_return <expression> must be used instead of
the usual return <expression> syntax. Because of this, the
PW_TRY and PW_TRY_ASSIGN macros are not usable within
coroutines. PW_CO_TRY and PW_CO_TRY_ASSIGN should be
used instead.
For a more detailed explanation of 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.
A CoroContext, which holds a
pw::Allocator, must be passed to any function that
returns a Coro. This allocator is used to allocate the
coroutine frame. If allocation fails, the resulting Coro will be invalid
and will immediately return a Ready(Status::Internal()) result when polled.
This design makes coroutine memory usage explicit and controllable.
Passing data between coroutines#
Just like when Passing data between tasks, there are two patterns for sending data between coroutines, with very much the same solutions.
This section just briefly describes how to co_await the data, as all the
details around construction and sending a value are the same as
Passing data between tasks.
Single values#
As with the non-coroutine case, pw_async2 provides the
OnceSender and OnceReceiver helpers for sending and receiving a one-time value.
As OnceReceiver satisfies the Pend()
interface. This means your coroutine can just co_await the receiver instance
to obtain the value.
1 pw::Result<int> value = co_await int_receiver;
2 if (!value.ok()) {
3 PW_LOG_ERROR(
4 "OnceSender was destroyed without sending a message! Outrageous :(");
5 co_return pw::Status::Cancelled();
6 }
7 PW_LOG_INFO("Got an int: %d", *value);
Like in the non-coroutine case, the value is wrapped as a Result<T> in case
of error.