pw_async2#
Cooperative async tasks for embedded
Experimental C++17
Simple Ownership: Say goodbye to that jumble of callbacks and shared state! Complex tasks with many concurrent elements can be expressed by simply combining smaller tasks.
Efficient: No dynamic memory allocation required.
Pluggable: Your existing event loop, work queue, or task scheduler can run the
Dispatcher
without any extra threads.Coroutine-capable: C++20 coroutines and Rust
async fn
work just like other tasks, and can easily plug into an existingpw_async2
systems.
pw::async2::Task
is Pigweed’s async primitive. Task
objects
are cooperatively-scheduled “threads” which yield to the
pw::async2::Dispatcher
when waiting. When the Task
is able to make
progress, the Dispatcher
will run it again. For example:
#include "pw_async2/dispatcher.h"
#include "pw_async2/poll.h"
#include "pw_result/result.h"
using ::pw::async2::Context;
using ::pw::async2::Poll;
using ::pw::async2::Ready;
using ::pw::async2::Pending;
using ::pw::async2::Task;
class ReceiveAndSend : public Task {
public:
ReceiveAndSend(Receiver receiver, Sender sender):
receiver_(receiver), sender_(sender) {}
Poll<> Pend(Context& cx) {
if (!send_future_) {
// ``PendReceive`` checks for available data or errors.
//
// If no data is available, it will grab a ``Waker`` from
// ``cx.Waker()`` and return ``Pending``. When data arrives,
// it will call ``waker.Wake()`` which tells the ``Dispatcher`` to
// ``Pend`` this ``Task`` again.
Poll<pw::Result<Data>> new_data = receiver_.PendReceive(cx);
if (new_data.is_pending()) {
// The ``Task`` is still waiting on data. Return ``Pending``,
// yielding to the dispatcher. ``Pend`` will be called again when
// data becomes available.
return Pending();
}
if (!new_data->ok()) {
PW_LOG_ERROR("Receiving failed: %s", data->status().str());
// The ``Task`` completed;
return Ready();
}
Data& data = **new_data;
send_future_ = sender_.Send(std::move(data));
}
// ``PendSend`` attempts to send ``data_``, returning ``Pending`` if
// ``sender_`` was not yet able to accept ``data_``.
Poll<pw::Status> sent = send_future_.Pend(cx);
if (sent.is_pending()) {
return Pending();
}
if (!sent->ok()) {
PW_LOG_ERROR("Sending failed: %s", sent->str());
}
return Ready();
}
private:
Receiver receiver_;
Sender sender_;
// ``SendFuture`` is some type returned by `Sender::Send` that offers a
// ``Pend`` method similar to the one on ``Task``.
std::optional<SendFuture> send_future_ = std::nullopt;
};
Tasks can then be run on a pw::async2::Dispatcher
using the
pw::async2::Dispatcher::Post()
method:
#include "pw_async2/dispatcher.h"
int main() {
ReceiveAndSendTask task(SomeMakeReceiverFn(), SomeMakeSenderFn());
Dispatcher dispatcher;
dispatcher.Post(task);
dispatcher.RunUntilComplete(task);
return 0;
}
Coroutines#
C++20 users can also define tasks using coroutines!
1#include "pw_allocator/allocator.h"
2#include "pw_async2/coro.h"
3#include "pw_result/result.h"
4
5namespace {
6
7using ::pw::OkStatus;
8using ::pw::Result;
9using ::pw::Status;
10using ::pw::allocator::Allocator;
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 ``Allocator`` 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> ReceiveAndSend(CoroContext&,
24 MyReceiver receiver,
25 MySender sender) {
26 pw::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 pw::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.
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 the
documentation on the pw::async2::Coro
type.
C++ API reference#
-
class Task#
A task which may complete one or more asynchronous operations.
The
Task
interface is commonly implemented by users wishing to schedule work on an asynchronousDispatcher
. To do this, users may subclassTask
, providing an implementation of theDoPend
method which advances the state of theTask
as far as possible before yielding back to theDispatcher
.This process works similarly to cooperatively-scheduled green threads or coroutines, with a
Task
representing a single logical “thread” of execution. Unlike some green thread or coroutine implementations,Task
does not imply a separately-allocated stack:Task
state is most commonly stored in fields of theTask
subclass.Once defined by a user,
Task
s may be run by passing them to aDispatcher
viaDispatcher::Post
. TheDispatcher
will thenPend
theTask
every time that theTask
indicates it is able to make progress.Note that
Task
objects must not be destroyed while they are actively beingPend
’d by aDispatcher
. To protect against this, be sure to do one of the following:Use dynamic lifetimes by creating
Task
objects that continue to live until they receive aDoDestroy
call.Create
Task
objects whose stack-based lifetimes outlive their associatedDispatcher
.Call
Deregister
on theTask
prior to its destruction. NOTE thatDeregister
may not be called from inside theTask
’s ownPend
method.
Subclassed by pw::async2::CoroOrElseTask, pw::async2::PendFuncTask< Func >, pw::async2::PendableAsTask< Pendable >, pw::async2::internal::AllocatedTask< Pendable >
Public Functions
-
inline Poll Pend(Context &cx)#
A public interface for
DoPend
.DoPend
is normally invoked by aDispatcher
after aTask
has beenPost
ed.This wrapper should only be called by
Task
s delegating to otherTask
s. For example, aclass MainTask
might have separate fields forTaskA
andTaskB
, and could invokePend
on these types within its ownDoPend
implementation.
-
bool IsRegistered() const#
Whether or not the
Task
is registered with aDispatcher
.Returns
true
after thisTask
is passed toDispatcher::Post
until one of the following occurs:This
Task
returnsReady
from itsPend
method.Task::Deregister
is called.The associated
Dispatcher
is destroyed.
-
void Deregister()#
Deregisters this
Task
from the linkedDispatcher
and any associatedWaker
values.This must not be invoked from inside this task’s
Pend
function, as this will result in a deadlock.NOTE: If this task’s
Pend
method is currently being run on the dispatcher, this method will block untilPend
completes.NOTE: This method sadly cannot guard against the dispatcher itself being destroyed, so this method must not be called concurrently with destruction of the dispatcher associated with this
Task
.Note that this will not destroy the underlying
Task
.
-
inline void Destroy()#
A public interface for
DoDestroy
.DoDestroy
is normally invoked by aDispatcher
after aPost
edTask
has completed.This should only be called by
Task
s delegating to otherTask
s.
-
template<typename T = ReadyType>
class Poll# A value that may or may not be ready yet.
Poll<T>
most commonly appears as the return type of an function that checks the current status of an asynchronous operation. If the operation has completed, it returns withReady(value)
. Otherwise, it returnsPending
to indicate that the operations has not yet completed, and the caller should try again in the future.Poll<T>
itself is “plain old data” and does not change on its own. To check the current status of an operation, the caller must invoke thePoll<T>
returning function again and examine the newly returnedPoll<T>
.Public Functions
-
Poll() = delete#
Basic constructors.
-
template<typename U, internal_poll::EnableIfImplicitlyConvertible<T, const U&> = 0>
inline constexpr Poll(
)# Constructs a new
Poll<T>
from aPoll<U>
whereT
is constructible fromU
.To avoid ambiguity, this constructor is disabled if
T
is also constructible fromPoll<U>
.This constructor is explicit if and only if the corresponding construction of
T
fromU
is explicit.
-
inline constexpr bool IsReady() const noexcept#
Returns whether or not this value is
Ready
.
-
inline constexpr bool IsPending() const noexcept#
Returns whether or not this value is
Pending
.
-
inline constexpr Poll Readiness() const noexcept#
Returns a
Poll<>
without the inner value whose readiness matches that ofthis
.
-
inline constexpr T &value() & noexcept#
Returns the inner value.
This must only be called if
IsReady()
returnedtrue
.
-
inline constexpr const T *operator->() const noexcept#
Accesses the inner value.
This must only be called if
IsReady()
returnedtrue
.
-
Poll() = delete#
-
template<typename T, typename ...Args>
constexpr Poll<T> pw::async2::Ready(std::in_place_t, Args&&... args)# Returns a value indicating completion with some result (constructed in-place).
-
template<typename T>
constexpr Poll<T> pw::async2::Ready(T &&value)# Returns a value indicating completion with some result.
-
inline constexpr PendingType pw::async2::Pending()#
Returns a value indicating that an operation was not yet able to complete.
-
class Context#
Context for an asynchronous
Task
.This object contains resources needed for scheduling asynchronous work, such as the current
Dispatcher
and theWaker
for the current task.Context
s are most often created byDispatcher
s, which pass them intoTask::Pend
.Public Functions
-
inline Context(Dispatcher &dispatcher, Waker &waker)#
Creates a new
Context
containing the currently-runningDispatcher
and aWaker
for the currentTask
.
-
inline Dispatcher &dispatcher()#
The
Dispatcher
on which the currentTask
is executing.This can be used for spawning new tasks using
dispatcher().Post(task);
.
-
void ReEnqueue()#
Queues the current
Task::Pend
to run again in the future, possibly after other work is performed.This may be used by
Task
implementations that wish to provide additional fairness by yielding to the dispatch loop rather than perform too much work in a single iteration.This is semantically equivalent to calling
GetWaker(...).Wake()
-
inline Context(Dispatcher &dispatcher, Waker &waker)#
-
class Waker#
An object which can respond to asynchronous events by queueing work to be done in response, such as placing a
Task
on aDispatcher
loop.Waker
s are often held by I/O objects, custom concurrency primitives, or interrupt handlers. Once the thing theTask
was waiting for is available,Wake
should be called so that theTask
is alerted and may process the event.Waker
s may be held for any lifetime, and will be automatically nullified when the underlyingDispatcher
orTask
is deleted.Waker
s are most commonly created byDispatcher
s, which pass them intoTask::Pend
via itsContext
argument.Public Functions
-
Waker &operator=(Waker &&other) noexcept#
Replace this
Waker
with another.This operation is guaranteed to be thread-safe.
-
void Wake() &&#
Wakes up the
Waker
’s creator, alerting it that an asynchronous event has occurred that may allow it to make progress.Wake
operates on an rvalue reference (&&
) in order to indicate that the event that was waited on has been complete. This makes it possible to track the outstanding events that may cause aTask
to wake up and make progress.This operation is guaranteed to be thread-safe.
-
Waker Clone(WaitReason reason) &#
Creates a second
Waker
from thisWaker
.Clone
is made explicit in order to allow for easier tracking of the differentWaker
s that may wake up aTask
.The
WaitReason
argument can be used to provide information about what event theWaker
is waiting on. This can be useful for debugging purposes.This operation is guaranteed to be thread-safe.
-
bool IsEmpty() const#
Returns whether this
Waker
is empty.Empty wakers are those that perform no action upon wake. These may be created either via the default no-argument constructor or by calling
Clear
orstd::move
on aWaker
, after which the moved-fromWaker
will be empty.This operation is guaranteed to be thread-safe.
-
Waker &operator=(Waker &&other) noexcept#
-
class Dispatcher : public pw::async2::DispatcherImpl<Dispatcher>#
-
template<std::constructible_from<pw::Status> T>
class Coro# An asynchronous coroutine which implements the C++20 coroutine API.
Why coroutines?#
Coroutines allow a series of asynchronous operations to be written as straight line code. Rather than manually writing a state machine, users can
co_await
any Pigweed asynchronous value (types with aPoll<T> Pend(Context&)
method).Allocation#
Pigweed’s
Coro<T>
API supports checked, fallible, heap-free allocation. The first argument to any coroutine function must be aCoroContext
(or a reference to one). This allows the coroutine to allocate space for asynchronously-held stack variables using the allocator member of theCoroContext
.Failure to allocate coroutine “stack” space will result in the
Coro<T>
returningStatus::Invalid()
.Creating a coroutine function#
To create a coroutine, a function must:
Have an annotated return type of
Coro<T>
whereT
is some type constructible frompw::Status
, such aspw::Status
orpw::Result<U>
.Use
co_return <value>
rather thanreturn <value>
for anyreturn
statements. This also requires the use ofPW_CO_TRY
andPW_CO_TRY_ASSIGN
rather thanPW_TRY
andPW_TRY_ASSIGN
.Accept a value convertible to
pw::allocator::Allocator&
as its first argument. This allocator will be used to allocate storage for coroutine stack variables held across aco_await
point.
Using co_await#
Inside a coroutine function,
co_await <expr>
can be used on any type with aPoll<T> Pend(Context&)
method. The result will be a value of typeT
.Example#
1#include "pw_allocator/allocator.h" 2#include "pw_async2/coro.h" 3#include "pw_result/result.h" 4 5namespace { 6 7using ::pw::OkStatus; 8using ::pw::Result; 9using ::pw::Status; 10using ::pw::allocator::Allocator; 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 ``Allocator`` 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> ReceiveAndSend(CoroContext&, 24 MyReceiver receiver, 25 MySender sender) { 26 pw::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 pw::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
Public Types
-
class CoroContext#
Context required for creating and executing coroutines.
Public Functions
-
inline explicit CoroContext(pw::allocator::Allocator &alloc)#
Creates a
CoroContext
which will allocate coroutine state usingalloc
.
-
inline explicit CoroContext(pw::allocator::Allocator &alloc)#
C++ Utilities#
-
template<typename Pendable>
Task *pw::async2::AllocateTask(pw::allocator::Allocator &allocator, Pendable &&pendable)# Creates a
Task
by dynamically allocatingTask
memory fromallocator
.Returns
nullptr
on allocation failure.Pendable
must have aPoll<> Pend(Context&)
method.allocator
must outlive the resultingTask
.
-
template<typename Pendable, typename ...Args>
Task *pw::async2::AllocateTask(
)# Creates a
Task
by dynamically allocatingTask
memory fromallocator
.Returns
nullptr
on allocation failure.Pendable
must have aPoll<> Pend(Context&)
method.allocator
must outlive the resultingTask
.
-
class CoroOrElseTask : public pw::async2::Task#
A
Task
that delegates to a providedCoro<Status>>
and executes anor_else
handler function on failure.
-
template<typename Func>
class PendFuncTask : public pw::async2::Task# A
Task
that delegates to a provided functionfunc
.The provided
func
may be any callable (function, lambda, or similar) which accepts aContext&
and returns aPoll<>
.The resulting
Task
will implementPend
by invokingfunc
.
-
template<typename Pendable>
class PendableAsTask : public pw::async2::Task# A
Task
that delegates to a type with aPend
method.The wrapped type must have a
Pend
method which accepts aContext&
and return aPoll<>
.If
Pendable
is a pointer,PendableAsTask
will dereference it and attempt to invokePend
.