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 existing pw_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 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. The best way to ensure this is to create Task objects that continue to live until they receive a DoDestroy call or which outlive their associated Dispatcher.

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 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<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 : public pw::async2::DispatcherImpl<Dispatcher>#

C++ Utilities#

template<typename Func>
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.