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 asynchronous Dispatcher. To do this, users may subclass Task, providing an implementation of the DoPend method which advances the state of the Task as far as possible before yielding back to the Dispatcher.

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 the Task subclass.

Once defined by a user, Task s may be run by passing them to a Dispatcher via Dispatcher::Post. The Dispatcher will then Pend the Task every time that the Task indicates it is able to make progress.

Note that Task objects must not be destroyed while they are actively being Pend’d by a Dispatcher. 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 a DoDestroy call.

  • Create Task objects whose stack-based lifetimes outlive their associated Dispatcher.

  • Call Deregister on the Task prior to its destruction. NOTE that Deregister may not be called from inside the Task’s own Pend 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 a Dispatcher after a Task has been Post ed.

This wrapper should only be called by Task s delegating to other Task s. For example, a class MainTask might have separate fields for TaskA and TaskB, and could invoke Pend on these types within its own DoPend implementation.

bool IsRegistered() const#

Whether or not the Task is registered with a Dispatcher.

Returns true after this Task is passed to Dispatcher::Post until one of the following occurs:

void Deregister()#

Deregisters this Task from the linked Dispatcher and any associated Waker 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 until Pend 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 a Dispatcher after a Post ed Task has completed.

This should only be called by Task s delegating to other Task 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 other Task s from progressing. Because of this, Task s should not invoke blocking Dispatcher methods such as RunUntilComplete.

Task s should also avoid invoking RunUntilStalled` on their own Dispatcher``.

Returns Ready if complete, or Pending if the Task was not yet able to complete.

If Pending is returned, the Task must ensure it is woken up when it is able to make progress. To do this, Task::Pend must arrange for Waker::Wake to be called, either by storing a copy of the Waker away to be awoken by another system (such as an interrupt handler).

inline virtual void DoDestroy()#

Performs any necessary cleanup of Task memory after completion.

This may include calls to std::destroy_at(this), and may involve deallocating the memory for this Task itself.

Tasks implementations which wish to be reused may skip self-destruction here.

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 with Ready(value). Otherwise, it returns Pending 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 the Poll<T> returning function again and examine the newly returned Poll<T>.

Public Functions

Poll() = delete#

Basic constructors.

template<typename U, internal_poll::EnableIfImplicitlyConvertible<T, const U&> = 0>
inline constexpr Poll(
const Poll<U> &other,
)#

Constructs a new Poll<T> from a Poll<U> where T is constructible from U.

To avoid ambiguity, this constructor is disabled if T is also constructible from Poll<U>.

This constructor is explicit if and only if the corresponding construction of T from U 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 of this.

inline constexpr T &value() & noexcept#

Returns the inner value.

This must only be called if IsReady() returned true.

inline constexpr const T *operator->() const noexcept#

Accesses the inner value.

This must only be called if IsReady() returned true.

inline constexpr const T &operator*() const & noexcept#

Returns the inner value.

This must only be called if IsReady() returned true.

inline constexpr void IgnorePoll() const#

Ignores the Poll value.

This method does nothing except prevent no_discard or unused variable warnings from firing.

inline constexpr Poll pw::async2::Ready()#

Returns a value indicating completion.

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 the Waker for the current task.

Context s are most often created by Dispatcher s, which pass them into Task::Pend.

Public Functions

inline Context(Dispatcher &dispatcher, Waker &waker)#

Creates a new Context containing the currently-running Dispatcher and a Waker for the current Task.

inline Dispatcher &dispatcher()#

The Dispatcher on which the current Task 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()

Waker GetWaker(WaitReason reason)#

Returns a Waker which, when awoken, will cause the current task to be Pend’d by its dispatcher.

class Waker#

An object which can respond to asynchronous events by queueing work to be done in response, such as placing a Task on a Dispatcher loop.

Waker s are often held by I/O objects, custom concurrency primitives, or interrupt handlers. Once the thing the Task was waiting for is available, Wake should be called so that the Task is alerted and may process the event.

Waker s may be held for any lifetime, and will be automatically nullified when the underlying Dispatcher or Task is deleted.

Waker s are most commonly created by Dispatcher s, which pass them into Task::Pend via its Context 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 a Task to wake up and make progress.

This operation is guaranteed to be thread-safe.

Waker Clone(WaitReason reason) &#

Creates a second Waker from this Waker.

Clone is made explicit in order to allow for easier tracking of the different Wakers that may wake up a Task.

The WaitReason argument can be used to provide information about what event the Waker 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 or std::move on a Waker, after which the moved-from Waker will be empty.

This operation is guaranteed to be thread-safe.

inline void Clear()#

Clears this Waker.

After this call, Wake will no longer perform any action, and IsEmpty will return true.

This operation is guaranteed to be thread-safe.

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 run Task to completion. This method does not block.

After Post is called, Task::Pend will be invoked once. If Task::Pend does not complete, the Dispatcher will wait until the Task is “awoken”, at which point it will call Pend again until the Task completes.

This method is thread-safe and interrupt-safe.

inline Poll RunUntilStalled()#

Runs tasks until none are able to make immediate progress.

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 of pendable.

inline void RunToCompletion()#

Runs until all tasks complete.

inline void RunToCompletion(Task &task)#

Runs until task completes.

template<typename Pendable>
inline PendOutputOf<Pendable> RunPendableToCompletion(Pendable &pendable)#

Runs until pendable completes, returning the output of pendable.

inline pw::async2::backend::NativeDispatcher &native()#

Returns a reference to the native backend-specific dispatcher type.

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 a Poll<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 a CoroContext (or a reference to one). This allows the coroutine to allocate space for asynchronously-held stack variables using the allocator member of the CoroContext.

Failure to allocate coroutine “stack” space will result in the Coro<T> returning Status::Invalid().

Creating a coroutine function#

To create a coroutine, a function must:

  • Have an annotated return type of Coro<T> where T is some type constructible from pw::Status, such as pw::Status or pw::Result<U>.

  • Use co_return <value> rather than return <value> for any return statements. This also requires the use of PW_CO_TRY and PW_CO_TRY_ASSIGN rather than PW_TRY and PW_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 a co_await point.

Using co_await#

Inside a coroutine function, co_await <expr> can be used on any type with a Poll<T> Pend(Context&) method. The result will be a value of type T.

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

using promise_type = ::pw::async2::internal::CoroPromiseType<T>#

Used by the compiler in order to create a Coro<T> from a coroutine function.

Public Functions

inline bool IsValid() const#

Whether or not this Coro<T> is a valid coroutine.

This will return false if coroutine state allocation failed or if this Coro<T>::Pend method previously returned a Ready value.

inline Poll<T> Pend(Context &cx)#

Attempt to complete this coroutine, returning the result if complete.

Returns Status::Internal() if !IsValid(), which may occur if coroutine state allocation fails.

Public Static Functions

static inline Coro Empty()#

Creates an empty, invalid coroutine object.

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 using 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 the TimeProvider from which they were created.

Subclassed by pw::async2::SimulatedTimeProvider< Clock >

Public Functions

virtual Clock::time_point now() override = 0#

Returns the current time.

inline TimeFuture<Clock> WaitFor(typename Clock::duration delay)#

Queues the callback to be invoked after delay.

This method is thread-safe and can be invoked from callback but may not be interrupt-safe on all platforms.

inline TimeFuture<Clock> WaitUntil(typename Clock::time_point timestamp)#

Queues the callback to be invoked after timestamp.

This method is thread-safe and can be invoked from callback but may not be interrupt-safe on all platforms.

TimeProvider<chrono::SystemClock> &pw::async2::GetSystemTimeProvider()#

Returns a TimeProvider using the “real” SystemClock and SystemTimer.

template<typename Clock>
class SimulatedTimeProvider : public pw::async2::TimeProvider<Clock>#

A simulated TimeProvider suitable for testing APIs which use Timer.

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 and SetTime will trigger expired timers to run. However, if a timer is set for a time in the past and neither AdvanceTime nor SetTime are subsequently invoked, the timer will not have a chance to run until one of AdvanceTime, SetTime, or RunExpiredTimers has been called.

inline virtual Clock::time_point now() final#

Returns the current time.

Utilities#

template<typename Func>
void pw::async2::EnqueueHeapFunc(Dispatcher &dispatcher, Func &&func)#

Heap-allocates space for func and enqueues it to run on dispatcher.

func must be a no-argument callable that returns void.

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 allocating Task memory from allocator.

Returns nullptr on allocation failure. Pendable must have a Poll<> Pend(Context&) method. allocator must outlive the resulting Task.

template<typename Pendable, typename ...Args>
Task *pw::async2::AllocateTask(
pw::allocator::Allocator &allocator,
Args&&... args,
)#

Creates a Task by dynamically allocating Task memory from allocator.

Returns nullptr on allocation failure. Pendable must have a Poll<> Pend(Context&) method. allocator must outlive the resulting Task.

class CoroOrElseTask : public pw::async2::Task#

A Task that delegates to a provided Coro<Status>> and executes an or_else handler function on failure.

Public Functions

inline CoroOrElseTask(Coro<Status> &&coro, pw::Function<void(Status)> &&or_else)#

Create a new Task which runs coro, invoking or_else on any non-OK status.

inline void SetCoro(Coro<Status> &&coro)#

Non-atomically sets coro.

The task must not be Posted when coro is changed.

inline void SetErrorHandler(pw::Function<void(Status)> &&or_else)#

Non-atomically sets or_else.

The task must not be Posted when or_else is changed.

template<typename Func = Function<Poll<>(Context&)>>
class PendFuncTask : public pw::async2::Task#

A Task that delegates to a provided function func.

The provided func may be any callable (function, lambda, or similar) which accepts a Context& and returns a Poll<>.

The resulting Task will implement Pend by invoking func.

Public Functions

inline PendFuncTask(Func &&func)#

Create a new Task which delegates Pend to func.

See class docs for more details.

template<typename Pendable>
class PendableAsTask : public pw::async2::Task#

A Task that delegates to a type with a Pend method.

The wrapped type must have a Pend method which accepts a Context& and return a Poll<>.

If Pendable is a pointer, PendableAsTask will dereference it and attempt to invoke Pend.

Public Functions

inline PendableAsTask(Pendable &&pendable)#

Create a new Task which delegates Pend to pendable.

See class docs for more details.

template<typename T>
std::pair<OnceSender<T>, OnceReceiver<T>> pw::async2::MakeOnceSenderAndReceiver()#

Construct a pair of OnceSender and OnceReceiver.

template<typename T>
class OnceSender#

OnceSender sends the value received by the OnceReceiver it is constructed with. It must be constructed using MakeOnceSenderAndReceiver. OnceSender is thread safe and may be used on a different thread than OnceReceiver.

Public Functions

template<typename ...Args>
inline void emplace(Args&&... args)#

Construct the sent value in place and wake the OnceReceiver.

template<typename T>
class OnceReceiver#

OnceReceiver receives the value sent by the OnceSender it is constructed with. It must be constructed using MakeOnceSenderAndReceiver. OnceReceiver::Pend() is used to poll for the value sent by OnceSender. OnceReceiver is thread safe and may be used on a different thread than OnceSender.

Public Functions

inline Poll<Result<T>> Pend(Context &cx)#

Returns Ready with a result containing the value once the value has been assigned. If the sender is destroyed before sending a value, a Cancelled result will be returned.

template<typename T>
std::pair<OnceRefSender<T>, OnceRefReceiver<T>> pw::async2::MakeOnceRefSenderAndReceiver(T &value)#

Constructs a joined pair of OnceRefSender and OnceRefReceiver.

Parameters:

value[in] The reference to be mutated by the sender. It must mot be read or modified until either OnceRefSender indicates Ready() or either the OnceRefSender or OnceRefReceiver is destroyed.

template<typename T>
class OnceRefSender#

OnceRefSender mutates the reference received by the OnceReceiver it is constructed with. It must be constructed using MakeOnceRefSenderAndReceiver. OnceRefSender is thread safe and may be used on a different thread than OnceRefReceiver.

Public Functions

inline void Set(const T &value)#

Copy assigns the reference and awakens the receiver.

inline void Set(T &&value)#

Move assigns the reference and awakens the receiver.

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 within func. This should be a simple modification. After all modifications are complete, Commit should be called.

inline void Commit()#

When using ModifyUnsafe(), call Commit() after all modifications have been made to awaken the OnceRefReceiver.

template<typename T>
class OnceRefReceiver#

OnceRefReceiver is notified when the paired OnceRefSender modifies a reference. It must be constructed using MakeOnceRefSenderAndReceiver(). OnceRefReceiver::Pend() is used to poll for completion by OnceRefSender. OnceRefReceiver is thread safe and may be used on a different thread than OnceRefSender. However, the referenced value must not be modified from the time of construction until either OnceRefReceiver::Pend() returns Ready() or either of OnceRefReceiver or OnceRefSender is destroyed.

Public Functions

inline Poll<Status> Pend(Context &cx)#

Returns Ready with an ok status when the modification of the reference is complete. If the sender is destroyed before updating the reference, a cancelled status is returned.