Reference#
pw_async2: Cooperative async tasks for embedded
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 >, pw::async2::internal::PendableAsTaskWithOutput< Pendable >, pw::async2::internal::RunHeapFuncTask< Func >
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.
Private Functions
-
bool TryDeregister()#
Attempts to deregister this task.
If the task is currently running, this will return false and the task will not be deregistered.
-
virtual Poll DoPend(Context&) = 0#
Attempts to advance this
Task
to completion.This method should not perform synchronous waiting, as doing so may block the main
Dispatcher
loop and prevent otherTask
s from progressing. Because of this,Task
s should not invoke blockingDispatcher
methods such asRunUntilComplete
.Task
s should also avoid invokingRunUntilStalled` on their own
Dispatcher``.Returns
Ready
if complete, orPending
if theTask
was not yet able to complete.If
Pending
is returned, theTask
must ensure it is woken up when it is able to make progress. To do this,Task::Pend
must arrange forWaker::Wake
to be called, either by storing a copy of theWaker
away to be awoken by another system (such as an interrupt handler).
-
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<std::remove_reference_t<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:
Waker waker; PW_ASYNC_STORE_WAKER(cx, waker, ...); std::move(waker).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.
-
void InternalCloneInto(Waker &waker_out) &#
INTERNAL-ONLY: users should use the
PW_ASYNC_CLONE_WAKER
macro.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
.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#
-
PW_ASYNC_STORE_WAKER#
Arguments:
Context& cx, Waker& waker_out, StringLiteral wait_reason_string
Stores a waker associated with the current context in
waker_out
. Whenwaker_out
is later awoken withpw::async2::Waker::Wake()
, thepw::async2::Task
associated withcx
will be awoken and itsDoPend
method will be invoked again.
-
PW_ASYNC_CLONE_WAKER#
Arguments:
Waker& waker_in, Waker& waker_out, StringLiteral wait_reason_string
Stores a waker associated with
waker_in
inwaker_out
. Whenwaker_out
is later awoken withpw::async2::Waker::Wake()
, thepw::async2::Task
associated withwaker_in
will be awoken and itsDoPend
method will be invoked again.
-
class Dispatcher#
A single-threaded cooperatively-scheduled runtime for async tasks.
Public Functions
-
Dispatcher() = default#
Constructs a new async Dispatcher.
-
inline void Post(Task &task)#
Tells the
Dispatcher
to runTask
to completion. This method does not block.After
Post
is called,Task::Pend
will be invoked once. IfTask::Pend
does not complete, theDispatcher
will wait until theTask
is “awoken”, at which point it will callPend
again until theTask
completes.This method is thread-safe and interrupt-safe.
-
inline Poll RunUntilStalled(Task &task)#
Runs tasks until none are able to make immediate progress, or until
task
completes.Returns whether
task
completed.
-
template<typename Pendable>
inline Poll<PendOutputOf<Pendable>> RunPendableUntilStalled(Pendable &pendable)# Runs tasks until none are able to make immediate progress, or until
pendable
completes.Returns a
Poll
containing the possible output ofpendable
.
-
inline void RunToCompletion()#
Runs until all tasks complete.
-
Dispatcher() = default#
-
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_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
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)#
-
template<typename Clock>
class TimeProvider : public pw::chrono::VirtualClock<Clock># A factory for time and timers.
This extends the
VirtualClock
interface with the ability to create async timers.TimeProvider
is designed to be dependency-injection friendly so that code that uses time and timers is not bound to real wall-clock time. This is particularly helpful for testing timing-sensitive code without adding manual delays to tests (which often results in flakiness and long-running tests).Note that
Timer
objects must not outlive theTimeProvider
from which they were created.Subclassed by pw::async2::SimulatedTimeProvider< Clock >
-
TimeProvider<chrono::SystemClock> &pw::async2::GetSystemTimeProvider()#
Returns a
TimeProvider
using the “real”SystemClock
andSystemTimer
.
-
template<typename Clock>
class SimulatedTimeProvider : public pw::async2::TimeProvider<Clock># A simulated
TimeProvider
suitable for testing APIs which useTimer
.Public Functions
-
inline void AdvanceTime(typename Clock::duration duration)#
Advances the simulated time and runs any newly-expired timers.
-
inline bool AdvanceUntilNextExpiration()#
Advances the simulated time until the next point at which a timer would fire.
Returns whether any timers were waiting to be run.
-
inline void SetTime(typename Clock::time_point new_now)#
Modifies the simulated time and runs any newly-expired timers.
WARNING: Use of this function with a timestamp older than the current
now()
will violate the is_monotonic clock attribute. We don’t like it when time goes backwards!
-
inline void RunExpiredTimers()#
Explicitly run expired timers.
Calls to this function are not usually necessary, as
AdvanceTime
andSetTime
will trigger expired timers to run. However, if a timer is set for a time in the past and neitherAdvanceTime
norSetTime
are subsequently invoked, the timer will not have a chance to run until one ofAdvanceTime
,SetTime
, orRunExpiredTimers
has been called.
-
inline void AdvanceTime(typename Clock::duration duration)#
Utilities#
-
template<typename Func>
void pw::async2::EnqueueHeapFunc(Dispatcher &dispatcher, Func &&func)# Heap-allocates space for
func
and enqueues it to run ondispatcher
.func
must be a no-argument callable that returnsvoid
.This function requires heap allocation using
new
be available.
-
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 ...Pendables>
class Join# A pendable value which joins together several separate pendable values.
It will only return
Ready
once all of the individual pendables have returnedReady
. The resultingReady
value contains a tuple of the results of joined pendable values.
-
template<typename Func = Function<Poll<>(Context&)>>
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
.
-
template<typename T>
std::pair<OnceSender<T>, OnceReceiver<T>> pw::async2::MakeOnceSenderAndReceiver()# Construct a pair of
OnceSender
andOnceReceiver
.
-
template<typename T>
class OnceSender# OnceSender
sends the value received by theOnceReceiver
it is constructed with. It must be constructed usingMakeOnceSenderAndReceiver
.OnceSender
is thread safe and may be used on a different thread thanOnceReceiver
.Public Functions
-
template<typename ...Args>
inline void emplace(Args&&... args)# Construct the sent value in place and wake the
OnceReceiver
.
-
template<typename ...Args>
-
template<typename T>
class OnceReceiver# OnceReceiver
receives the value sent by theOnceSender
it is constructed with. It must be constructed usingMakeOnceSenderAndReceiver
.OnceReceiver::Pend()
is used to poll for the value sent byOnceSender
.OnceReceiver
is thread safe and may be used on a different thread thanOnceSender
.Public Functions
-
template<typename ...Args>
inline explicit OnceReceiver(Args&&... value_args)# Create an already completed OnceReceiver by constructing the value with
value_args
.
-
template<typename ...Args>
-
template<typename T>
std::pair<OnceRefSender<T>, OnceRefReceiver<T>> pw::async2::MakeOnceRefSenderAndReceiver(T &value)# Constructs a joined pair of
OnceRefSender
andOnceRefReceiver
.- Parameters:
value – [in] The reference to be mutated by the sender. It must mot be read or modified until either
OnceRefSender
indicatesReady()
or either theOnceRefSender
orOnceRefReceiver
is destroyed.
-
template<typename T>
class OnceRefSender# OnceRefSender
mutates the reference received by theOnceReceiver
it is constructed with. It must be constructed usingMakeOnceRefSenderAndReceiver
.OnceRefSender
is thread safe and may be used on a different thread thanOnceRefReceiver
.Public Functions
-
inline void ModifyUnsafe(pw::Function<void(T&)> func)#
Care must be taken not to save the reference passed to
func
or to call any other Once*Sender/Once*Receiver APIs from withinfunc
. This should be a simple modification. After all modifications are complete,Commit
should be called.
-
inline void Commit()#
When using
ModifyUnsafe()
, callCommit()
after all modifications have been made to awaken theOnceRefReceiver
.
-
inline void ModifyUnsafe(pw::Function<void(T&)> func)#
-
template<typename T>
class OnceRefReceiver# OnceRefReceiver
is notified when the pairedOnceRefSender
modifies a reference. It must be constructed usingMakeOnceRefSenderAndReceiver()
.OnceRefReceiver::Pend()
is used to poll for completion byOnceRefSender
.OnceRefReceiver
is thread safe and may be used on a different thread thanOnceRefSender
. However, the referenced value must not be modified from the time of construction until eitherOnceRefReceiver::Pend()
returnsReady()
or either ofOnceRefReceiver
orOnceRefSender
is destroyed.