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,
TimerTaskcompletes 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:
Tasks that communicate with each other via Channels.
Tasks that wait on multiple futures.
Browse the Learn more section on the pw_async2 homepage
for more guidance on building correct, efficient firmware with pw_async2.