pw_async2 is built upon a few fundamental concepts that work together to provide a powerful asynchronous runtime.
A Task is the basic unit of execution, analogous to a cooperatively scheduled thread. It’s an object that represents a job to be done, like reading from a sensor or processing a network packet. Users implement a task’s logic in its DoPend() method.
The Dispatcher is the cooperative scheduler. It maintains a queue of tasks that are ready to be polled.
A user writing a task implements DoPend() , but they too call Pend() when calling other tasks. Pend() is also the interface of “pendable objects” used throughout pw_async2 such as Select , Join , and coroutines .
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 put the task to sleep and then run it again later.
Ready() : The task has finished its work. The Dispatcher should not poll it again.
The dispatcher runs a task by calling Pend() , which is a non-virtual wrapper around DoPend() . The task attempts to make progress and communicates to the dispatcher what state it’s in by returning one of these values:
“Informed” polling with wakers#
When a task’s DoPend method returns Pending() , the task must ensure that something will eventually trigger it to be run again. This is the “informed” part of the model: the task informs the Dispatcher when it’s ready to be polled again. This is achieved using a Waker.
Before returning Pending() , the task must obtain its Waker from the Context and store it somewhere that’s accessible to the event source. Common event sources include interrupt handlers and timer managers. When the event occurs, the event source calls Waker::Wake() on the stored Waker . The Wake() call notifies the Dispatcher that the task is ready to make progress. The Dispatcher moves the task back into its run queue and polls it again in the future.
This mechanism prevents the Dispatcher from having to wastefully poll tasks that aren’t ready, allowing it to sleep and save power when no work can be done.
The following diagram illustrates the interaction between these components:
sequenceDiagram participant e as Event participant d as Dispatcher participant t as Task e->>d: Post(Task) d->>d: Add task to run queue d->>t: Run task via Task::DoPend() t->>t: Task is waiting for data, cannot complete t->>e: Store Waker for future wake-up t->>d: Return Pending() d->>d: Remove task from run queue (now sleeping) e->>e: The data the task needs arrives e->>d: Wake task via Waker::Wake() d->>d: Re-add task to run queue d->>t: Run task again via Task::DoPend() t->>t: Task uses data and runs to completion t->>d: Return Ready() d->>d: Deregister the completed task