Quickstart#

pw_async2: Cooperative async tasks for embedded

pw_async2 is Pigweed’s cooperatively scheduled async framework. pw_async2 makes it easier to build correct, efficient asynchronous firmware. This quickstart sets you up with a minimal but complete pw_async2 project and explains key concepts along the way.

  • For a detailed conceptual overview of pw_async2, check out Informed poll.

  • For a more in-depth, hands-on introduction, try the Codelab.

 1#include "pw_async2/basic_dispatcher.h"
 2#include "pw_async2/future.h"
 3#include "pw_async2/system_time_provider.h"
 4#include "pw_log/log.h"
 5
 6using ::pw::async2::BasicDispatcher;
 7using ::pw::async2::GetSystemTimeProvider;
 8using ::std::chrono_literals::operator""ms;
 9
10int main() {
11  // The cooperative scheduler of pw_async2.
12  BasicDispatcher dispatcher;
13  // Create a task that only progresses after 100ms have passed.
14  auto timer = GetSystemTimeProvider().WaitFor(100ms);
15  TimerTask timer_task(timer);
16  // Queue the task with the dispatcher.
17  dispatcher.Post(timer_task);
18  // Run until all tasks are complete.
19  dispatcher.RunToCompletion();
20  return 0;
21}

Create a subclass of Task:

 1using ::pw::async2::Context;
 2using ::pw::async2::Poll;
 3using ::pw::async2::Task;
 4using ::pw::async2::TimeFuture;
 5using ::pw::chrono::SystemClock;
 6
 7class TimerTask : public Task {
 8 public:
 9  TimerTask(TimeFuture<SystemClock>& timer)
10      : Task(PW_ASYNC_TASK_NAME("TimerTask")), timer_(timer) {}
11
12 private:
13  // Contains the task's core async logic.
14  Poll<> DoPend(Context& cx) override;
15  TimeFuture<SystemClock>& timer_;
16};

Implement an override of DoPend():

 1Poll<> TimerTask::DoPend(Context& cx) {
 2  // Wait for the timer to complete.
 3  if (timer_.Pend(cx).IsPending()) {
 4    PW_LOG_INFO("Waiting…");
 5    // Notify the dispatcher that the task is stalled.
 6    return pw::async2::Pending();
 7  }
 8  PW_LOG_INFO("Done!");
 9  // Notify the dispatcher that the task is complete.
10  return pw::async2::Ready();
11}
 1pw_cc_test(
 2    name = "quickstart",
 3    srcs = ["quickstart.cc"],
 4    deps = [
 5        "//pw_async2:basic_dispatcher",
 6        "//pw_async2:system_time_provider",
 7        "//pw_log",
 8        "//pw_unit_test",
 9    ],
10)

Tip

The quickstart is based on the following files in upstream Pigweed:

You can build and test it yourself with the following command:

bazelisk test //pw_async2/examples:quickstart

See Contributing guide for help with setting up the upstream Pigweed repo.

Set up build rules#

Add a dependency on dispatcher. If you instantiate a concrete dispatcher such as basic_dispatcher, also depend on that dispatcher implementation. The dispatcher dependency pulls in most of the core pw_async2 API that you’ll always need.

 1pw_cc_test(
 2    name = "quickstart",
 3    srcs = ["quickstart.cc"],
 4    deps = [
 5        "//pw_async2:basic_dispatcher",
 6        "//pw_async2:system_time_provider",
 7        "//pw_log",
 8        "//pw_unit_test",
 9    ],
10)

See Bazel for help with creating a new Bazel project or integrating Pigweed into an existing Bazel project.

Note

The code above uses pw_cc_test because we unit test this example in upstream Pigweed. In your project you’ll want to use cc_binary or pw_cc_binary instead.

pw_executable("quickstart") {
  sources = [
    "quickstart.cc",
  ]
  deps = [
    "$dir_pw_async2",
    # …
  ]
}

See GN / Ninja for help with integrating Pigweed into an existing GN project.

add_executable(quickstart quickstart.cc)
target_link_libraries(quickstart PRIVATE pw_async2)

See CMake for help with integrating Pigweed into an existing CMake project.

Create a dispatcher and post a task#

The dispatcher is the cooperative scheduler of pw_async2. Tasks are logical collections of async work. You post tasks to the dispatcher, and the dispatcher drives the tasks to completion.

The highlighted lines below demonstrate creating a task, posting a task to a dispatcher, and running the tasks with a dispatcher:

 1#include "pw_async2/basic_dispatcher.h"
 2#include "pw_async2/future.h"
 3#include "pw_async2/system_time_provider.h"
 4#include "pw_log/log.h"
 5
 6using ::pw::async2::BasicDispatcher;
 7using ::pw::async2::GetSystemTimeProvider;
 8using ::std::chrono_literals::operator""ms;
 9
10int main() {
11  // The cooperative scheduler of pw_async2.
12  BasicDispatcher dispatcher;
13  // Create a task that only progresses after 100ms have passed.
14  auto timer = GetSystemTimeProvider().WaitFor(100ms);
15  TimerTask timer_task(timer);
16  // Queue the task with the dispatcher.
17  dispatcher.Post(timer_task);
18  // Run until all tasks are complete.
19  dispatcher.RunToCompletion();
20  return 0;
21}

The next section dives into the TimerTask implementation in detail.

Implement a task#

Each task represents a logical collection of async work. For example, when writing the firmware for a vending machine, you might dedicate one task to handling user input, another task to driving the item dispenser machinery, and yet another task to sending time series data to the cloud.

First, let’s study the main API of a single task. Your task class must be a subclass of pw::async2::Task and it must implement an override of DoPend(), as TimerTask demonstrates:

 1using ::pw::async2::Context;
 2using ::pw::async2::Poll;
 3using ::pw::async2::Task;
 4using ::pw::async2::TimeFuture;
 5using ::pw::chrono::SystemClock;
 6
 7class TimerTask : public Task {
 8 public:
 9  TimerTask(TimeFuture<SystemClock>& timer)
10      : Task(PW_ASYNC_TASK_NAME("TimerTask")), timer_(timer) {}
11
12 private:
13  // Contains the task's core async logic.
14  Poll<> DoPend(Context& cx) override;
15  TimeFuture<SystemClock>& timer_;
16};

Note

The dispatcher drives a task forward by invoking the task’s Pend() method, which is a non-virtual wrapper around DoPend().

The DoPend() implementation is where TimerTask progresses through its work asynchronously. DoPend() waits on a TimeFuture called timer_ to complete. timer_ finishes after 100ms and then never runs again. Futures like timer_ will be described more in the next section. For now, focus on the return values of the implementation. Notice how the implementation returns Pending() in one case, and Ready() in another case:

 1Poll<> TimerTask::DoPend(Context& cx) {
 2  // Wait for the timer to complete.
 3  if (timer_.Pend(cx).IsPending()) {
 4    PW_LOG_INFO("Waiting…");
 5    // Notify the dispatcher that the task is stalled.
 6    return pw::async2::Pending();
 7  }
 8  PW_LOG_INFO("Done!");
 9  // Notify the dispatcher that the task is complete.
10  return pw::async2::Ready();
11}

All DoPend() implementations follow this general pattern:

  • The task is not able to progress for some reason so it returns pw::async2::Pending() to notify the dispatcher that it’s stalled. The dispatcher sleeps the task.

    Tip

    Helpers like PW_TRY_READY and PW_TRY_READY_ASSIGN can reduce the boilerplate of handling a stalled task.

  • Something informs the dispatcher that the task is able to make more progress. This will be explained in the next section. The dispatcher runs the task again.

  • On the second run, TimerTask completes all of its work. The task returns pw::async2::Ready() to signal to the dispatcher that it has finished and no longer needs to be polled.

Wait for a future#

If you run this example, you’ll see Waiting… logged only once:

INF  Waiting…
INF  Done!

This suggests that there’s no busy polling. So how does the polling work? timer_ is the key to understanding the flow. timer_ is a future, the main async primitive in pw_async2. A future is a value that may not be ready yet. Notice how cx is passed to the timer’s Pend() method:

 1Poll<> TimerTask::DoPend(Context& cx) {
 2  // Wait for the timer to complete.
 3  if (timer_.Pend(cx).IsPending()) {
 4    PW_LOG_INFO("Waiting…");
 5    // Notify the dispatcher that the task is stalled.
 6    return pw::async2::Pending();
 7  }
 8  PW_LOG_INFO("Done!");
 9  // Notify the dispatcher that the task is complete.
10  return pw::async2::Ready();
11}

When the future is ready, the future informs the dispatcher that its parent task can make more progress and therefore should be polled again. This is why the pw_async2 programming model is called Informed poll.

Note

When the task invokes the future’s Pend() method, the task provides a pw::async2::Context instance. This Context instance is how the future informs the dispatcher.

Learn more#

Posting tasks to the dispatcher and implementing tasks that wait on futures is the core of all pw_async2 systems. Of course, in a real-world project, you may need to handle more complex interactions, such as:

Browse the Learn more section on the pw_async2 homepage for more guidance on building correct, efficient firmware with pw_async2.