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 work just like other tasks, and can easily plug into an existing pw_async2 system.

pw::async2::Task is Pigweed’s async primitive. Task objects are cooperatively-scheduled “threads” which yield to the pw::async2::Dispatcher when waiting. When the Task is able to make progress, the Dispatcher will run it again. For example:

 1#include "pw_async2/dispatcher.h"
 2#include "pw_async2/poll.h"
 3#include "pw_async2/try.h"
 4#include "pw_log/log.h"
 5#include "pw_result/result.h"
 6
 7namespace {
 8
 9using ::pw::async2::Context;
10using ::pw::async2::Pending;
11using ::pw::async2::Poll;
12using ::pw::async2::Ready;
13using ::pw::async2::Task;
14
15class MyReceiver;
16class MySender;
17
18// Receive then send that data asynchronously. If the reader or writer aren't
19// ready, the task suspends when TRY_READY invokes ``return Pending()``.
20class ReceiveAndSend final : public Task {
21 public:
22  ReceiveAndSend(MyReceiver receiver, MySender sender)
23      : receiver_(receiver), sender_(sender), state_(kReceiving) {}
24
25  Poll<> DoPend(Context& cx) final {
26    switch (state_) {
27      case kReceiving: {
28        PW_TRY_READY_ASSIGN(auto new_data, receiver_.PendReceive(cx));
29        if (!new_data.ok()) {
30          PW_LOG_ERROR("Receiving failed: %s", new_data.status().str());
31          return Ready();  // Completes the task.
32        }
33        // Start transmitting and switch to transmitting state.
34        send_future_ = sender_.Send(std::move(*new_data));
35        state_ = kTransmitting;
36      }
37        [[fallthrough]];
38      case kTransmitting: {
39        PW_TRY_READY_ASSIGN(auto sent, send_future_->Pend(cx));
40        if (!sent.ok()) {
41          PW_LOG_ERROR("Sending failed: %s", sent.str());
42        }
43        return Ready();  // Completes the task.
44      }
45    }
46  }
47
48 private:
49  MyReceiver receiver_;  // Can receive data async.
50  MySender sender_;      // Can send data async.
51  std::optional<SendFuture> send_future_ = std::nullopt;
52
53  enum State { kTransmitting, kReceiving };
54  State state_;
55};
56
57}  // namespace
 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

Tasks can then be run on a pw::async2::Dispatcher using the pw::async2::Dispatcher::Post() method:

1  auto task = ReceiveAndSend(std::move(receiver), std::move(sender));
2  Dispatcher dispatcher;
3  // Registers `task` to run on the dispatcher.
4  dispatcher.Post(task);
5  // Sets the dispatcher to run until all `Post`ed tasks have completed.
6  dispatcher.RunToCompletion();
Quickstart & guides

How to:

  • Use dispatchers to coordinate tasks

  • Pass data between tasks

  • Use coroutines

And more.

Reference

API reference for:

  • Task

  • Dispatcher

  • CoRo

And more.

Backends

You can fulfill the pw_async2 interface with a Pigweed-provided backend or roll your own.

Pigweed blog: C++20 coroutines

A blog post on how Pigweed implements coroutines without heap allocation, and challenges encountered along the way.