blob: 012c36853c3b19c78408888e45ecac26e08be405 [file]
.. _module-pw_async2-informed-poll:
=============
Informed poll
=============
.. pigweed-module-subpage::
:name: pw_async2
The *informed poll* programming model is the core design philosophy behind
``pw_async2``. Informed poll is an alternative to callback-based asynchronous
programming that simplifies state management for complex concurrent operations.
This overview helps you build a mental model of how informed poll works.
It's easier to build robust and correct asynchronous systems with
``pw_async2`` when you've got a thorough understanding of the informed poll
programming model.
.. _module-pw_async2-informed-poll-summary:
-------
Summary
-------
The central idea is that asynchronous work is encapsulated in :cc:`Tasks
<pw::async2::Task>`, which are similar to `green threads`_. Instead of
registering callbacks for different events, a central :cc:`Dispatcher
<pw::async2::Dispatcher>` *polls* tasks to see if they can make progress.
Tasks drive one or more asynchronous operations to completion. The async
operations communicate whether or not their values are ready via :ref:`futures
<module-pw_async2-futures>`, the basic async primitive in ``pw_async2``. When
none of the future values are ready, the task notifies the dispatcher that it
can't progress, and the dispatcher sleeps the task. When a future's value
becomes ready, the future uses a :cc:`Waker <pw::async2::Waker>` to *inform* the
dispatcher that the task associated with the :cc:`Waker <pw::async2::Waker>`
can make more progress and therefore should be polled again.
.. mermaid::
sequenceDiagram
participant d as Dispatcher
participant t as Task
participant o as Async Operation
d->>t: Run task
t->>o: Start async operation
o->>t: Vend a future
t->>t: Poll the future, not ready yet
t->>d: Sleep
o->>o: Future is ready
o->>d: Wake the task
d->>t: Run task
t->>t: Consume the future's value
.. _module-pw_async2-informed-poll-components:
---------------
Core components
---------------
This section provides more explanation of the core components of the
``pw_async2`` framework: :ref:`dispatchers
<module-pw_async2-informed-poll-components-dispatcher>`, :ref:`tasks
<module-pw_async2-informed-poll-components-task>`, :ref:`futures
<module-pw_async2-informed-poll-components-future>`, and :ref:`wakers
<module-pw_async2-informed-poll-components-waker>`.
The following diagram summarizes how asynchronous work generally progresses
in a ``pw_async2`` system.
.. mermaid::
stateDiagram-v2
direction LR
Dispatcher --> Task
Task --> Future
Future --> Waker
Waker --> Dispatcher
.. _module-pw_async2-informed-poll-components-dispatcher:
Dispatcher: The cooperative scheduler
=====================================
The dispatcher maintains a queue of tasks that are ready to be polled. The
dispatcher runs whatever task has been ready the longest. There is
no concept of task `priority`_. The dispatcher drives a task forward by
calling the task's :cc:`Pend() <pw::async2::Task::Pend>` method, which is a
non-virtual wrapper around :cc:`DoPend() <pw::async2::Task::DoPend>`. The core
logic of a task is implemented in its ``DoPend()`` method.
.. note::
``Pend()`` is also the core interface for futures and coroutines.
When the dispatcher is informed that a task can't make any more progress,
the dispatcher removes the task from its ready queue and places the task in its
sleep queue. When the dispatcher is informed that a sleeping task can make more
progress, it places the task back into its ready queue.
.. _module-pw_async2-informed-poll-components-task:
Tasks: Logical collections of async work
========================================
:cc:`Tasks <pw::async2::Task>` are similar to `green threads`_, i.e. threads
that are `cooperatively scheduled`_ by a runtime library, not `preemptively
scheduled`_ by an underlying OS. Tasks usually represent logical collections of
work. For example, in the :ref:`pw_async2 codelab <module-pw_async2-codelab>`,
where you write firmware for a vending machine, one task handles user input
(coin insertions and item selection) while another task manages the item
dispenser machinery (controlling the motors to dispense an item and detecting
when an item has dropped).
.. _module-pw_async2-informed-poll-components-task-state:
Communicating task state to the dispatcher
------------------------------------------
A task communicates to the dispatcher what state it's in by returning one of
these values in its ``Pend()`` implementation:
* :cc:`Ready() <pw::async2::Ready>`: The task has finished its work. The
``Dispatcher`` should not poll it again.
* :cc:`Pending() <pw::async2::Pending>`: The task is not yet finished
because it is waiting for an external event. E.g. it's waiting for a timer to
finish or for data to arrive. The dispatcher should sleep the task and
then run it again later.
.. _module-pw_async2-informed-poll-components-future:
Futures: The basic async primitive
==================================
Tasks invoke asynchronous operations that return :ref:`futures
<module-pw_async2-futures>`, which are values that may not be ready yet. In the
vending machine example mentioned in
:ref:`module-pw_async2-informed-poll-components-task`, coin insertions, item
selections, motor control, and item drop detection are all examples of async
operations.
Like tasks, futures use ``Ready()`` and ``Pending()`` to communicate whether
they're complete or not. The main difference is that futures can return a
value.
A task's primary role is often to poll multiple futures to completion and
coordinate the values that it's receiving from the futures. Futures are always
composable, which makes it easier for the task to manage complex asynchronous
logic coming from many different async operations.
Futures are always owned by a parent task. The task is responsible for holding
the state of the futures it owns and polling them all to completion, or
canceling them.
.. _module-pw_async2-informed-poll-components-waker:
Wakers: Progress updates for the dispatcher
===========================================
When a task signals to the dispatcher that it can't make any more progress, the
task must ensure that something will eventually trigger it to be run again.
This is accomplished via :cc:`Wakers <pw::async2::Waker>`. When a future's
value is ready, the future signals a waker to inform the dispatcher that its
parent task can make more progress. This mechanism prevents the ``Dispatcher``
from having to wastefully poll tasks that aren't ready, allowing the task to
sleep and save power when no work can be done.
Wakers are an important concept in the informed poll model, but in your code
they are often an implementation detail that you usually don't need to think
about. Pigweed-provided futures like :cc:`ValueFuture
<pw::async2::ValueFuture>` automatically store and invoke wakers on your
behalf.
------------------------------------------------
Comparison with traditional callback-based async
------------------------------------------------
The typical way to structure asynchronous operations in system code is through
callbacks invoked when the operation completes. While familiar, this approach
leads to issues in several areas:
- State management: callers often need to invoke a series of async operations
in order where callbacks trigger other callbacks, requiring complicated
context management.
- Ownership and lifetimes: it can be unclear who owns the data passed to a
callback and how long it is valid.
- Execution context: the callback could run on a different thread or in an ISR,
making certain operations unsafe to perform.
- Debugging: stack traces don't include much useful information, and it can be
difficult to determine who originally started the operation.
``pw_async2`` addresses these issues by promoting:
- Encapsulating state: Tasks and futures are objects that hold their own state.
There is no need to manually pack context into ``void*`` arguments.
- Explicit ownership: The task that owns the future manages its lifetime. If the
task is destroyed, its futures are also destroyed, preventing use-after-free
and similar lifetime bugs.
- Predictable execution: All tasks run on the dispatcher they are posted to, so
you know exactly which thread and stack is executing your code.
- Inspectability: Since tasks are objects, they can be inspected in a debugger
or by ``pw_async2`` tooling to see exactly what they are waiting for.
.. _module-pw_async2-informed-poll-rust:
---------------------------------------
Comparison with Rust's informed polling
---------------------------------------
Informed polling was first proposed for Pigweed in :ref:`SEED 0112 <seed-0112>`.
SEED 0112 was inspired by Rust's informed polling model.
Informed polling in Rust is built around its `Future`_ trait. ``pw_async2``
futures are conceptually very similar.
A key difference is that in Rust, `async` functions and blocks automatically
generate state machines that implement the `Future` trait. In ``pw_async2``,
you can achieve a similar ergonomic benefit by using
:ref:`coroutines <module-pw_async2-coro>`, which allow you to ``co_await``
futures. Without coroutines, you manually manage the state of futures within a
:cc:`Task <pw::async2::Task>`.
As a result of this, Pigweed's ``Future`` concept includes states in its core
API---empty, pendable, and complete---to make them easier to work with manually
when writing a chain of async operations within a ``Task``. An empty future is
uninitialized and does not represent a pending operation, indicating that the
task should start one. Similarly, a completed future signals that the operation
has completed and the task should move on to its next step.
---------
Evolution
---------
Since the proposal in :ref:`SEED 0112 <seed-0112>`, the design of ``pw_async2``
has evolved in several ways. The most significant change was the formalization
of futures as the standardized interface for asynchronous operations, replacing
ad-hoc "pendable functions" as originally proposed.
"Pendable functions" were simply any C++ function that returned a ``Poll<T>``.
While low-overhead and flexible, this design lacked a standard way to manage the
lifetime and ownership of an asynchronous operation's state and ambiguity about
who could run the operation and when. The lack of a standardized interface also
made these functions difficult to compose.
.. _Future: https://doc.rust-lang.org/std/future/trait.Future.html
.. _Stream: https://docs.rs/futures/latest/futures/prelude/trait.Stream.html
.. _green thread: https://en.wikipedia.org/wiki/Green_thread
.. _green threads: https://en.wikipedia.org/wiki/Green_thread
.. _cooperative scheduler: https://en.wikipedia.org/wiki/Cooperative_multitasking
.. _cooperatively scheduled: https://en.wikipedia.org/wiki/Cooperative_multitasking
.. _preemptively scheduled: https://en.wikipedia.org/wiki/Preemption_(computing)#Preemptive_multitasking
.. _priority: https://en.wikipedia.org/wiki/Priority_queue