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
The pendable function interface requirement, this means your
coroutine can just co_await
the receiver instance to obtain the value.
1 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 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.
Multiple values#
To use pw::InlineAsyncQueue or
pw::InlineAsyncDeque with co_await
, an
adapter is needed that exposes a Pend
that invokes the correct member
function in the containers (either PendHasSpace
or PendNotEmpty
).
PendableFor does this for you, and supports member functions or free functions.
For sending, the producing coroutine has to wait for there to be space before
trying to add to the queue. Here we co_await
the result of
PendableFor<&Queue::PendHasSpace>(queue, 1)
to wait for space for one
element to be available.
1 // Wait for there to be space in the queue before writing the next value.
2 co_await PendableFor<&Queue::PendHasSpace>(queue, 1);
3 queue.push(value);
Receiving values is similar. The receiving has to wait for there to be values before trying to remove them from the queue.
1 // Wait for there to be something to read.
2 co_await PendableFor<&Queue::PendNotEmpty>(queue);
3 const int result = queue.front();
4 queue.pop();
A complete example for using pw::InlineAsyncQueue this way can be found in pw_async2/examples/inline_async_queue_with_coro_test.cc, and you can try it yourself with:
bazelisk run --config=cxx20 //pw_async2/examples:inline_async_queue_with_coro_test