Coroutines#
pw_async2: Cooperative async tasks for embedded
For projects using C++20, pw_async2
provides first-class support for
coroutines via pw::async2::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
pw::async2::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 pw::async2::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 pw::async2::CoroContext, which holds a
pw::Allocator, must be passed to any function that
returns a pw::async2::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
pw::async2::OnceSender and pw::async2::OnceReceiver
helpers for sending and receiving a one-time value.
As pw::async2::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 DoPend
that invokes
the correct member function in the containers (either PendHasSpace
or
PendNotEmpty
).
1auto QueueHasSpace(Queue& queue) {
2 return PendFuncAwaitable(
3 [&queue](Context& cx) { return queue.PendHasSpace(cx); });
4}
5
6auto QueueNotEmpty(Queue& queue) {
7 return PendFuncAwaitable(
8 [&queue](Context& cx) { return queue.PendNotEmpty(cx); });
9}
For sending, the producing coroutine has to wait for there to be space before trying to add to the queue.
1 // Wait for there to be space in the queue before writing the next value.
2 co_await QueueHasSpace(queue);
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 QueueNotEmpty(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