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 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 Dispatcher
using the Dispatcher::Post
method:
#include "pw_async2/dispatcher.h"
int main() {
ReceiveAndSendTask task(SomeMakeReceiverFn(), SomeMakeSenderFn());
Dispatcher dispatcher;
dispatcher.Post(task);
dispatcher.RunUntilComplete(task);
return 0;
}
Roadmap#
Coming soon: C++20 users can also define tasks using coroutines!
#include "pw_async2/dispatcher.h"
#include "pw_async2/poll.h"
#include "pw_result/result.h"
using ::pw::async2::CoroutineTask;
CoroutineTask ReceiveAndSend(Receiver receiver, Sender sender) {
pw::Result<Data> data = co_await receiver.Receive(cx);
if (!data.ok()) {
PW_LOG_ERROR("Receiving failed: %s", data.status().str());
return;
}
pw::Status sent = co_await sender.Send(std::move(data));
if (!sent.ok()) {
PW_LOG_ERROR("Sending failed: %s", sent.str());
}
}
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
. The best way to ensure this is to createTask
objects that continue to live until they receive aDoDestroy
call or which outlive their associatedDispatcher
.Subclassed by pw::async2::PendFuncTask< Func >, pw::async2::PendableAsTask< Pendable >
-
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>#
C++ Utilities#
-
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
.