Quickstart & guides#
pw_async2: Cooperative async tasks for embedded
Quickstart#
This quickstart outlines the general workflow for integrating pw_async2
into a project. It’s based on the following files in upstream Pigweed:
The example app can be built and run in upstream Pigweed with the following command:
bazelisk run //pw_async2/examples:count --config=cxx20
1. Set up build rules#
All pw_async2
projects must add a dependency on the dispatcher
target.
This target defines the pw::async2::Task
class, an asynchronous
unit of work analogous to a thread, as well as the
pw::async2::Dispatcher
class, an event loop used to run Task
instances to completion.
Add a dependency on @pigweed//pw_async2:dispatcher
in BUILD.bazel
:
1pw_cc_binary(
2 name = "count",
3 srcs = ["count.cc"],
4 target_compatible_with = incompatible_with_mcu(),
5 deps = [
6 "//pw_allocator:libc_allocator",
7 "//pw_async2:allocate_task",
8 "//pw_async2:coro",
9 "//pw_async2:coro_or_else_task",
10 "//pw_async2:dispatcher",
11 "//pw_async2:system_time_provider",
12 "//pw_chrono:system_clock",
13 "//pw_log",
14 "//pw_result",
15 "//pw_status",
16 ],
17)
Add a dependency on $dir_pw_async2:dispatcher
in BUILD.gn
:
1 pw_executable("count") {
2 deps = [
3 "$dir_pw_allocator:libc_allocator",
4 "$dir_pw_async2:allocate_task",
5 "$dir_pw_async2:coro",
6 "$dir_pw_async2:coro_or_else_task",
7 "$dir_pw_async2:dispatcher",
8 "$dir_pw_async2:system_time_provider",
9 "$dir_pw_chrono:system_clock",
10 "$dir_pw_log",
11 "$dir_pw_result",
12 "$dir_pw_status",
13 ]
14 sources = [ "count.cc" ]
15 }
16
2. Inject dependencies#
Interfaces which wish to add new tasks to the event loop should accept and
store a Dispatcher&
reference.
1 Counter(Dispatcher& dispatcher,
2 Allocator& allocator,
3 TimeProvider<SystemClock>& time)
4 : dispatcher_(&dispatcher), allocator_(&allocator), time_(&time) {}
This allows the interface to call dispatcher->Post(some_task)
in order to
run asynchronous work on the dispatcher’s event loop.
3. Post one-shot work to the dispatcher#
Simple, one-time work can be queued on the dispatcher via
pw::async2::EnqueueHeapFunc()
.
4. Post tasks to the dispatcher#
Async work that involves a series of asynchronous operations should be
made into a task. This can be done by either implementing a custom task
(see Implementing tasks) or
by writing a C++20 coroutine (see pw::async2::Coro
) and storing it
in a pw::async2::CoroOrElseTask
.
1 // Posts a new asynchronous task which will count up to `times`, one count
2 // per `period`.
3 void StartCounting(SystemClock::duration period, int times) {
4 CoroContext coro_cx(*allocator_);
5 Coro<Status> coro = CountCoro(coro_cx, period, times);
6 Task* new_task =
7 AllocateTask<CoroOrElseTask>(*allocator_, std::move(coro), [](Status) {
8 PW_LOG_ERROR("Counter coroutine failed to allocate.");
9 });
10
11 // The newly allocated task will be free'd by the dispatcher
12 // upon completion.
13 dispatcher_->Post(*new_task);
14 }
The resulting task must either be stored somewhere that has a lifetime longer
than the async operations (such as in a static or as a member of a long-lived
class) or dynamically allocated using pw::async2::AllocateTask()
.
Finally, the interface instructs the dispatcher to run the task by invoking
pw::async2::Dispatcher::Post()
.
See //pw_async2/examples/count.cc to view the complete example.
5. Build with an appropriate toolchain#
If using coroutines, remember to build your project with a toolchain
that supports C++20 at minimum (the first version of C++ with coroutine
support). For example, in upstream Pigweed a --config=cxx20
must be
provided when building and running the example:
bazelisk build //pw_async2/examples:count --config=cxx20
Other examples#
To see another example of pw_async2
working in a minimal project,
check out the following directories of Pigweed’s quickstart/bazel repo:
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:
pw::async2::CoroOrElseTask
: Delegates to a provided coroutine and executes anor_else
handler function on failure.pw::async2::PendFuncTask
: Delegates to a provided function.pw::async2::PendableAsTask
: Delegates to a type with apw::async2::Pend()
method.pw::async2::AllocateTask()
: Creates a concrete subclass ofTask
, just likePendableAsTask
, but the created task is dynamically allocated and frees the associated memory upon completion.
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:
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:
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 callpw::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 invokedWaker
.
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.