Quickstart & guides#

pw_async2: Cooperative async tasks for embedded

Guides#

Implementing tasks#

pw::async2::Task instances complete one or more asynchronous operations. They are the top-level “thread” primitives of pw_async2.

You can use one of the concrete subclasses of Task that Pigweed provides:

Or you can subclass Task yourself. See pw::async2::Task for more guidance on subclassing.

How a dispatcher manages tasks#

The purpose of a pw::async2::Dispatcher is to keep track of a set of pw::async2::Task objects and run them to completion. The dispatcher is essentially a scheduler for cooperatively-scheduled (non-preemptive) threads (tasks).

While a dispatcher is running, it waits for one or more tasks to waken and then advances each task by invoking its pw::async2::Task::DoPend() method. The DoPend method is typically implemented manually by users, though it is automatically provided by coroutines.

If the task is able to complete, DoPend will return Ready, in which case the task is then deregistered from the dispatcher.

If the task is unable to complete, DoPend must return Pending and arrange for the task to be woken up when it is able to make progress again. Once the task is rewoken, the task is re-added to the Dispatcher queue. The dispatcher will then invoke DoPend once more, continuing the cycle until DoPend returns Ready and the task is completed.

The following sequence diagram summarizes the basic workflow:

sequenceDiagram participant e as External Event e.g. Interrupt participant d as Dispatcher participant t as Task e->>t: Init Task e->>d: Register task via Dispatcher::Post(Task) d->>d: Add task to queue d->>t: Run task via Task::DoPend() t->>t: Task is waiting for data and can't yet complete t->>e: Arrange for rewake via PW_ASYNC_STORE_WAKER t->>d: Indicate that task is not complete via Pending() d->>d: Remove task from queue d->>d: Go to sleep because task queue is empty e->>e: The data that the task needs has arrived e->>d: Rewake via Waker::Wake() d->>d: Re-add task to queue d->>t: Run task via Task::DoPend() t->>t: Task runs to completion t->>d: Indicate that task is complete via Ready() d->>d: Deregister the task

Implementing invariants for pendable functions#

Any Pend-like function or method similar to pw::async2::Task::DoPend() that can pause when it’s not able to make progress on its task is known as a pendable function. When implementing a pendable function, make sure that you always uphold the following invariants:

Note

Exactly which APIs are considered pendable?

If it has the signature (Context&, ...) -> Poll<T>, then it’s a pendable function.

Arranging future completion of incomplete tasks#

When your pendable function can’t yet complete:

  1. Do one of the following to make sure the task rewakes when it’s ready to make more progress:

    • Delegate waking to a subtask. Arrange for that subtask’s pendable function to wake this task when appropriate.

    • Arrange an external wakeup. Use PW_ASYNC_STORE_WAKER to store the task’s waker somewhere, and then call pw::async2::Waker::Wake() from an interrupt or another thread once the event that the task is waiting for has completed.

    • Re-enqueue the task with pw::async2::Context::ReEnqueue(). This is a rare case. Usually, you should just create an immediately invoked Waker.

  2. Make sure to return pw::async2::Pending to signal that the task is incomplete.

In other words, whenever your pendable function returns pw::async2::Pending, you must guarantee that pw::async2::Context::Wake() is called once in the future.

For example, one implementation of a delayed task might arrange for its Waker to be woken by a timer once some time has passed. Another case might be a messaging library which calls Wake() on the receiving task once a sender has placed a message in a queue.

Cleaning up complete tasks#

When your pendable function has completed, make sure to return pw::async2::Ready to signal that the task is complete.

Passing data between tasks#

Astute readers will have noticed that the Wake method does not take any arguments, and DoPoll does not provide the task being polled with any values!

Unlike callback-based interfaces, tasks (and the libraries they use) are responsible for storage of the inputs and outputs of events. A common technique is for a task implementation to provide storage for outputs of an event. Then, upon completion of the event, the outputs will be stored in the task before it is woken. The task will then be invoked again by the dispatcher and can then operate on the resulting values.

This common pattern is implemented by the pw::async2::OnceSender and pw::async2::OnceReceiver types (and their ...Ref counterparts). These interfaces allow a task to asynchronously wait for a value:

 1#include "pw_async2/dispatcher.h"
 2#include "pw_async2/dispatcher_native.h"
 3#include "pw_async2/once_sender.h"
 4#include "pw_async2/poll.h"
 5#include "pw_log/log.h"
 6#include "pw_result/result.h"
 7
 8namespace {
 9
10using ::pw::Result;
11using ::pw::async2::Context;
12using ::pw::async2::OnceReceiver;
13using ::pw::async2::OnceSender;
14using ::pw::async2::Pending;
15using ::pw::async2::Poll;
16using ::pw::async2::Ready;
17using ::pw::async2::Task;
18
19class ReceiveAndLogValueTask : public Task {
20 public:
21  ReceiveAndLogValueTask(OnceReceiver<int>&& int_receiver)
22      : int_receiver_(std::move(int_receiver)) {}
23
24 private:
25  Poll<> DoPend(Context& cx) override {
26    Poll<Result<int>> value = int_receiver_.Pend(cx);
27    if (value.IsPending()) {
28      return Pending();
29    }
30    if (!value->ok()) {
31      PW_LOG_ERROR(
32          "OnceSender was destroyed without sending a message! Outrageous :(");
33    }
34    PW_LOG_INFO("Got an int: %d", **value);
35    return Ready();
36  }
37
38  OnceReceiver<int> int_receiver_;
39};
40
41}  // namespace
 1#include "pw_async2/coro.h"
 2#include "pw_async2/once_sender.h"
 3#include "pw_log/log.h"
 4#include "pw_result/result.h"
 5
 6namespace {
 7
 8using ::pw::OkStatus;
 9using ::pw::Status;
10using ::pw::async2::Coro;
11using ::pw::async2::CoroContext;
12using ::pw::async2::OnceReceiver;
13using ::pw::async2::OnceSender;
14
15Coro<Status> ReceiveAndLogValue(CoroContext&, OnceReceiver<int> int_receiver) {
16  Result<int> value = co_await int_receiver;
17  if (!value.ok()) {
18    PW_LOG_ERROR(
19        "OnceSender was destroyed without sending a message! Outrageous :(");
20    co_return Status::Cancelled();
21  }
22  PW_LOG_INFO("Got an int: %d", *value);
23  co_return OkStatus();
24}
25
26}  // namespace

More primitives (such as MultiSender and MultiReceiver) are in-progress. Users who find that they need other async primitives are encouraged to contribute them upstream to pw::async2!

Coroutines#

C++20 users can define tasks using coroutines!

 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

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 pw::async2::Coro.

Timing#

When using pw::async2, timing functionality should be injected by accepting a pw::async2::TimeProvider (most commonly TimeProvider<SystemClock> when using the system’s built-in time_point and duration types).

pw::async2::TimeProvider allows for easily waiting for a timeout or deadline using the pw::async2::TimePoint::WaitFor() and pw::async2::TimePoint::WaitUntil() methods.

Additionally, code which uses pw::async2::TimeProvider for timing can be tested with simulated time using pw::async2::SimulatedTimeProvider. Doing so helps avoid timing-dependent test flakes and helps ensure that tests are fast since they don’t need to wait for real-world time to elapse.

Frequently asked questions (FAQs)#