pw_async2#

Cooperative async tasks for embedded

Experimental C++17

pw_async2 is a cooperatively scheduled asynchronous framework for C++, optimized for use in resource-constrained systems. It helps you write complex, concurrent applications without the overhead of traditional preemptive multithreading. The design prioritizes efficiency, minimal resource usage (especially memory), and testability.

Benefits#

  • 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.

Example#

Informed poll is the core design philosophy behind pw_async2. Task is the main async primitive. It’s a cooperatively scheduled “thread” which yields to the Dispatcher when waiting. When a Task is able to make progress, the Dispatcher runs it again:

 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

Tasks can then be run on a Dispatcher using the 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();

Learn more#

Informed poll

The core design philosophy behind pw_async2. We strongly encourage all pw_async2 users to internalize this concept before attempting to use pw_async2!

Codelab

Get hands-on experience with the core concepts of pw_async2 by building a simple, simulated vending machine.

Quickstart

How to quickly integrate pw_async2 into your project and start using basic features.

Guides

How to use dispatchers to coordinate tasks, pass data between tasks, and more.

Reference

C/C++ API reference for Task, Dispatcher, CoRo, and more.

Code size analysis

Reports on the code size cost of adding pw_async2 to a system.

Backends

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

Coroutines

How to define tasks with coroutines, allocate memory, perform async operations from coroutines, and more.