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/channel.h"
3#include "pw_async2/coro.h"
4#include "pw_log/log.h"
5#include "pw_result/result.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
17/// ``receiver`` and 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, ``Coro<Status>``
21/// will return ``Status::Internal()``.
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 can be passed to co_await,
which will return with a T when the result is ready.
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.