xref: /aosp_15_r20/external/pigweed/seed/0112.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. _seed-0112:
2
3======================
40112: Async Poll Model
5======================
6.. seed::
7   :number: 0112
8   :name: Async Poll Model
9   :status: Accepted
10   :proposal_date: 2023-9-19
11   :cl: 168337
12   :authors: Taylor Cramer
13   :facilitator: Wyatt Hepler
14
15-------
16Summary
17-------
18This SEED proposes the development of a new “informed-Poll”-based pw::async
19library. The “informed Poll” model, popularized by
20`Rust’s Future trait, <https://doc.rust-lang.org/std/future/trait.Future.html>`_
21offers an alternative to callback-based APIs. Rather than invoking a separate
22callback for every event, the informed Poll model runs asynchronous ``Task`` s.
23
24A ``Task`` is an asynchronous unit of work. Informed Poll-based asynchronous
25systems use ``Task`` s similar to how synchronous systems use threads.
26Users implement the ``Poll`` function of a ``Task`` in order to define the
27asynchronous behavior of a routine.
28
29.. code-block:: cpp
30
31   class Task {
32    public:
33     /// Does some work, returning ``Complete`` if done.
34     /// If not complete, returns ``Pending`` and arranges for `cx.waker()` to be
35     /// awoken when `Task:Poll` should be invoked again.
36     virtual pw::MaybeReady<Complete> Poll(pw::async::Context& cx);
37   };
38
39Users can start running a ``Task`` by ``Post`` ing it to a ``Dispatcher``.
40``Dispatcher`` s are asynchronous event loops which are responsible for calling
41``Poll`` every time the ``Task`` indicates that it is ready to make progress.
42
43This API structure allows Pigweed async code to operate efficiently, with low
44memory overhead, zero dynamic allocations, and simpler state management.
45
46Pigweed’s new async APIs will enable multi-step asynchronous operations without
47queuing multiple callbacks. Here is an example in which a proxy object receives
48data and then sends it out before completing:
49
50.. code-block:: cpp
51
52   class ProxyOneMessage : public Task {
53    public:
54     /// Proxies one ``Data`` packet from a ``Receiver`` to a ``Sender``.
55     ///
56     /// Returns:
57     ///   ``pw::async::Complete`` when the task has completed. This happens
58     ///     after a ``Data`` packet has been received and sent, or an error
59     ///     has occurred and been logged.
60     ///   ``pw::async::Pending`` if unable to complete. ``cx.waker()`` will be
61     ///     awoken when ``Poll`` should be re-invoked.
62     pw::async::MaybeReady<pw::async::Complete> Poll(pw::async::Context& cx) {
63       if (!send_future_) {
64         // ``PollRead`` checks for available data or errors.
65         pw::async::MaybeReady<pw::Result<Data>> new_data = receiver_.PollRead(cx);
66         if (new_data.is_pending()) {
67           return pw::async::Pending;
68         }
69         if (!new_data->ok()) {
70           PW_LOG_ERROR("Receiving failed: %s", data->status().str());
71           return pw::async::Complete;
72         }
73         Data& data = **new_data;
74         send_future_ = sender_.Send(std::move(data));
75       }
76       // ``PollSend`` attempts to send `data_`, returning `Pending` if
77       // `sender_` was not yet able to accept `data_`.
78       pw::async::MaybeReady<pw::Status> sent = send_future_.Poll(cx);
79       if (sent.is_pending()) {
80         return pw::async::Pending;
81       }
82       if (!sent->ok()) {
83         PW_LOG_ERROR("Sending failed: %s", sent->str());
84       }
85       return pw::async::Complete;
86     }
87
88    private:
89     // ``SendFuture`` is some type returned by `Sender::Send` that offers a
90     // ``Poll`` method similar to the one on ``Task``.
91     std::optional<SendFuture> send_future_;
92     // `receiver_` and `sender_` are provided by the `ProxyOneMessage` constructor.
93     Receiver receiver_;
94     Sender sender_;
95   };
96
97   // --- Usage ---
98   // ``static`` is used for simplicity, but real ``Task`` s can have temporary
99   // lifetimes.
100   static ProxyOneMessage proxy(receiver, sender);
101
102   // Runs `proxy` until it completes, either by successfully receiving and
103   // sending a message, or by exiting early after logging an error.
104   dispatcher.Post(proxy);
105
106--------
107Proposal
108--------
109This SEED proposes that Pigweed develop a set of async APIs and utilities
110designed around the informed Poll model. If early trials with partner teams are
111successful, this new library will be used as the basis for future async code in
112Pigweed.
113
114-----
115Goals
116-----
117The goals of this SEED are as follows:
118
119* Establish community consensus that informed ``Poll`` is the best async model
120  for Pigweed to pursue.
121* Outline an initial API for ``Dispatcher`` implementors (platform authors) and
122  top-level ``Task`` writers.
123
124----------
125Motivation
126----------
127The purpose of this SEED is to gather agreement that ``Poll``-based async
128APIs are worth pursuing. We believe that these APIs provide the needed support
129for:
130
131* Small code size
132* Environments without dynamic allocation
133* Creating reusable building blocks and high-level modules
134
135The current ``Task`` API is limited in these respects: a single ``Task`` must
136be created and stored for every individual asynchronous event. ``Task`` s
137cannot be reused, and the memory allocated for a ``Task`` can only be reclaimed
138after a ``Task`` has been completed or cancelled, resulting in complex
139semantics for multithreaded environments or those with interrupt-driven events.
140
141Completing a sequence of events therefore requires either dynamic allocation
142or statically saving a separate ``Task`` worth of memory for every kind of
143event that may occur.
144
145Additionally, every asynchronous layer requires introducing another round of
146callbacks whose semantics may be unclear and whose captures may add lifetime
147challenges.
148
149This proposal resolves these issues by choosing an alternative approach.
150
151-----------
152API Summary
153-----------
154
155A Note On Specificity
156=====================
157This SEED provides API outlines in order to more clearly explain the intended
158API direction. The specific function signatures shown here are not meant to be
159authoritative, and are subject to change. As the implementation develops
160support for more platforms and features, some additions, changes, or removals
161may be necessary and will be considered as part of the regular CL review
162process.
163
164With that in mind, asynchronous ``Task`` s in this model could adopt an API
165like the following:
166
167The ``MaybeReady`` Type
168=======================
169Functions return ``MaybeReady<T>`` to indicate that their result may or may
170not be available yet. ``MaybeReady<T>`` is a generic sum type similar to
171``std::optional<T>``. It has two variants, ``Ready(T)`` or ``Pending``.
172
173The API is similar to ``std::optional<T>``, but ``MaybeReady<T>`` provides extra
174semantic clarification that the absense of a value means that it is not ready
175yet.
176
177Paired with the ``Complete`` type, ``MaybeReady<Complete>`` acts like
178``bool IsComplete``, but provides more semantic information to the user than
179returning a simple ``bool``.
180
181.. code-block:: cpp
182
183   /// A value that is ready, and
184   template<typename T>
185   struct Ready<T> { value: T };
186
187   /// A content-less struct that indicates a not-ready value.
188   struct Pending {};
189
190   /// A value of type `T` that is possibly available.
191   ///
192   /// This is similar to ``std::optional<T>``, but provides additional
193   /// semantic indication that the value is not ready yet (still pending).
194   /// This can aid in making type signatures such as
195   /// ``MaybeReady<std::optional<Item>>`` easier to understand, and provides
196   /// clearer naming like `IsReady` (compared to ``has_value()``).
197   template<typename T>
198   class MaybeReady {
199    public:
200     /// Implicitly converts from ``T``,  ``Ready<T>`` or ``Pending``.
201     MaybeReady(T);
202     MaybeReady(Ready<T>);
203     MaybeReady(Pending);
204     bool IsReady();
205     T Value() &&;
206     ...
207   };
208
209   /// A content-less struct that indicates completion.
210   struct Complete {};
211
212Note that the ``Pending`` type takes no type arguments, and so can be created
213and returned from macros that don't know which ``T`` is returned by the
214function they are in. For example:
215
216.. code-block:: cpp
217
218   // Simplified assignment macro
219   #define PW_ASSIGN_IF_READY(lhs, expr) \
220     auto __priv = (expr);               \
221     if (!__priv.IsReady()) {            \
222       return pw::async::Pending;        \
223     }                                   \
224     lhs = std::move(__priv.Value())     \
225
226   MaybeReady<Bar> PollCreateBar(Context& cx);
227
228   Poll<Foo> DoSomething(Context& cx) {
229     PW_ASSIGN_IF_READY(Bar b, PollCreateBar(cx));
230     return CreateFoo();
231   }
232
233This is similar to the role of the ``std::nullopt_t`` type.
234
235The ``Dispatcher`` Type
236=======================
237Dispatchers are the event loops responsible for running ``Task`` s. They sleep
238when there is no work to do, and wake up when there are ``Task`` s ready to
239make progress.
240
241On some platforms, the ``Dispatcher`` may also provide special hooks in order
242to support single-threaded asynchronous I/O.
243
244.. code-block:: cpp
245
246   class Dispatcher {
247    public:
248     /// Tells the ``Dispatcher`` to run ``Task`` to completion.
249     /// This method does not block.
250     ///
251     /// After ``Post`` is called, ``Task::Poll`` will be invoked once.
252     /// If ``Task::Poll`` does not complete, the ``Dispatcher`` will wait
253     /// until the ``Task`` is "awoken", at which point it will call ``Poll``
254     /// again until the ``Task`` completes.
255     void Post(Task&);
256     ...
257   };
258
259The ``Waker`` Type
260==================
261A ``Waker`` is responsible for telling a ``Dispatcher`` when a ``Task`` is
262ready to be ``Poll`` ed again. This allows ``Dispatcher`` s to intelligently
263schedule calls to ``Poll`` rather than retrying in a loop (this is the
264"informed" part of "informed Poll").
265
266When a ``Dispatcher`` calls ``Task::Poll``, it provides a ``Waker`` that will
267enqueue the ``Task`` when awoken. ``Dispatcher`` s can implement this
268functionality by having ``Waker`` add the ``Task`` to an intrusive linked list,
269add a pointer to the ``Task`` to a ``Dispatcher``-managed vector, or by pushing
270a ``Task`` ID onto a system-level async construct such as ``epoll``.
271
272.. code-block:: cpp
273
274   /// An object which can respond to asynchronous events by queueing work to
275   /// be done in response, such as placing a ``Task`` on a ``Dispatcher`` loop.
276   class Waker {
277    public:
278     /// Wakes up the ``Waker``'s creator, alerting it that an asynchronous
279     /// event has occurred that may allow it to make progress.
280     ///
281     /// ``Wake`` operates on an rvalue reference (``&&``) in order to indicate
282     /// that the event that was waited on has been completed. This makes it
283     /// possible to track the outstanding events that may cause a ``Task`` to
284     /// wake up and make progress.
285     void Wake() &&;
286
287     /// Creates a second ``Waker`` from this ``Waker``.
288     ///
289     /// ``Clone`` is made explicit in order to allow for easier tracking of
290     /// the different ``Waker``s that may wake up a ``Task``.
291     Waker Clone(Token wait_reason_indicator) &;
292     ...
293   };
294
295The ``Wake`` function itself may be called by any system with knowledge that
296the ``Task`` is now ready to make progress. This can be done from an interrupt,
297from a separate task, from another thread, or from any other function that
298knows that the `Poll`'d type may be able to make progress.
299
300The ``Context`` Type
301====================
302``Context`` is a bundle of arguments supplied to ``Task::Poll`` that give the
303``Task`` information about its asynchronous environment. The most important
304parts of the ``Context`` are the ``Dispatcher``, which is used to ``Post``
305new ``Task`` s, and the ``Waker``, which is used to tell the ``Dispatcher``
306when to run this ``Task`` again.
307
308.. code-block:: cpp
309
310   class Context {
311    public:
312     Context(Dispatcher&, Waker&);
313     Dispatcher& Dispatcher();
314     Waker& Waker();
315     ...
316   };
317
318The ``Task`` Type
319=================
320Finally, the ``Task`` type is implemented by users in order to run some
321asynchronous work. When a new asynchronous "thread" of execution must be run,
322users can create a new ``Task`` object and send it to be run on a
323``Dispatcher``.
324
325.. code-block:: cpp
326
327   /// A task which may complete one or more asynchronous operations.
328   ///
329   /// ``Task`` s should be actively ``Poll`` ed to completion, either by a
330   /// ``Dispatcher`` or by a parent ``Task`` object.
331   class Task {
332    public:
333     MaybeReady<Complete> Poll(Context&);
334     ...
335    protected:
336     /// Returns whether or not the ``Task`` has completed.
337     ///
338     /// If the ``Task`` has not completed, `Poll::Pending` will be returned,
339     /// and `context.Waker()` will receive a `Wake()` call when the ``Task``
340     /// is ready to make progress and should be ``Poll`` ed again.
341     virtual MaybeReady<Complete> DoPoll(Context&) = 0;
342     ...
343   };
344
345This structure makes it possible to run complex asynchronous ``Task`` s
346containing multiple concurrent or sequential asynchronous events.
347
348------------------------------------
349Relationship to Futures and Promises
350------------------------------------
351The terms "future" and "promise" are unfortunately quite overloaded. This SEED
352does not propose a "method chaining" API (e.g. ``.AndThen([](..) { ... }``), nor
353is creating reference-counted, blocking handles to the output of other threads
354a la ``std::future``.
355
356Where this SEED refers to ``Future`` types (e.g. ``SendFuture`` in the summary
357example), it means only a type which offers a ``Poll(Context&)`` method and
358return some ``MaybeReady<T>`` value. This common pattern can be used to build
359various asynchronous state machines which optionally return a value upon
360completion.
361
362---------------------------------------------
363Usage In The Rust Ecosystem Shows Feasability
364---------------------------------------------
365The ``Poll``-based ``Task`` approach suggested here is similar to the one
366adopted by Rust's
367`Future type <https://doc.rust-lang.org/stable/std/future/trait.Future.html>`_.
368The ``Task`` class in this SEED is analogous to Rust's ``Future<Output = ()>``
369type. This model has proven usable on small environments without dynamic allocation.
370
371Due to compiler limitations, Rust's ``async fn`` language feature will often
372generate ``Future`` s which suffer from code size issues. However,
373manual implementations of Rust's ``Future`` trait (not using ``async fn``) do
374not have this issue.
375
376We believe the success of Rust's ``Poll``-based ``Future`` type demonstrates
377that the approach taken in this SEED can meet the needs of Pigweed users.
378
379---------
380Code Size
381---------
382`Some experiments have been done
383<https://pigweed-review.googlesource.com/c/pigweed/experimental/+/154570>`_
384to compare the size of the code generated by
385a ``Poll``-based approach with code generated with the existing ``pw::async``
386APIs. These experiments have so far found that the ``Poll``-based approach
387creates binaries with smaller code size due to an increased opportunity for
388inlining, static dispatch, and a smaller number of separate ``Task`` objects.
389
390The experimental ``pw_async_bench`` examples show that the ``Poll``-based
391approach offers more than 2kB of savings on a small ``Socket``-like example.
392
393------------------------
394The ``pw::async`` Facade
395------------------------
396This SEED proposes changing ``Dispatcher`` from a virtual base into a
397platform-specific concrete type.
398
399The existing ``pw::async::Dispatcher`` class is ``virtual`` in order to support
400use of an alternative ``Dispatcher`` implementation in tests. However, this
401approach assumes that ``Task`` s are capable of running on arbitrary
402implementations of the ``Dispatcher`` virtual interface. In practice, this is
403not the case.
404
405Different platforms will use different native ``Dispatcher`` waiting primitives
406including ``epoll``, ``kqueue``, IOCP, Fuchsia's ``libasync``/``zx_port``, and
407lower-level waiting primitives such as Zephyr's RTIO queue.
408
409Each of these primitives is strongly coupled with native async events, such as
410IO or buffer readiness. In order to support ``Dispatcher``-native IO events,
411IO objects must be able to guarantee that they are running on a compatible
412``Dispatcher``. In Pigweed, this can be accomplished through the use of the
413facade pattern.
414
415The facade patterns allows for concrete, platform-dependent definitions of the
416``Task``, ``Context``, ``Waker``, and ``Dispatcher`` types. This allows these
417objects to interact with one another as necessary to implement fast scheduling
418with minimal in-memory or code size overhead.
419
420This approach enables storing platform-specific per- ``Task`` scheduling details
421inline with the ``Task`` itself, enabling zero-allocation ``Task`` scheduling
422without the need for additional resource pools.
423
424This also allows for native integration with platform-specific I/O primitives
425including ``epoll``, ``kqueue``, IOCP, and others, but also lower-level
426waiting primitives such as Zephyr's RTIO queue.
427
428Testing
429=======
430Moving ``Dispatcher`` to a non-virtual facade means that the previous approach
431of testing with a ``FakeDispatcher`` would require a separate toolchain in
432order to provide a different instantiation of the ``Dispatcher`` type. However,
433we can adopt a simpler approach: the ``Dispatcher`` type can offer minimial
434testing primitives natively:
435
436.. code-block:: cpp
437
438   class Dispatcher {
439    public:
440     ...
441
442     /// Runs tasks until none are able to make immediate progress.
443     ///
444     /// Returns whether a ``Task`` was run.
445     bool RunUntilStalled();
446
447     /// Enable mock time, initializing the mock timer to some "zero"-like
448     /// value.
449     void InitializeMockTime();
450
451     /// Advances the mock timer forwards by ``duration``.
452     void AdvanceMockTime(chrono::SystemClock::duration duration);
453   };
454
455These primitives are sufficient for testing with mock time. They allow
456test authors to avoid deadlocks, timeouts, or race conditions.
457
458Downsides of Built-in Testing Functions
459---------------------------------------
460Requiring concrete ``Dispatcher`` types to include the testing functions above
461means that the production ``Dispatcher`` implementations will have code in them
462that is only needed for testing.
463
464However, these additions are minimal: mocking time introduces a single branch
465for each timer access, which is still likely to be more efficient than the
466virtual function call that was required under the previous model.
467
468Advantages of Built-in Testing Functions
469----------------------------------------
470Testing with a "real" ``Dispatcher`` implementation ensures that:
471
472* All ``pw::async`` platforms provide support for testing
473* The ``Dispatcher`` used for testing will support the same I/O operations and
474  features provided by the production ``Dispatcher``
475* Tests will run under conditions as-close-to-production as possible. This will
476  allow catching bugs that are caused by the interaction of the code and the
477  particular ``Dispatcher`` on which it runs.
478
479Enabling Dynamic ``Task`` Lifetimes
480===================================
481While some ``Task`` s may be static, others may not be. For these, we need a
482mechanism to ensure that:
483
484* ``Task`` resources are not destroyed while ``Waker`` s that may post them
485  to a ``Dispatcher`` remain.
486* ``Task`` resources are not destroyed while the ``Task`` itself is running
487  or is queued to run.
488
489In order to enable this, platforms should clear all ``Waker`` s referencing a
490``Task`` when the ``Task`` completes: that ``Task`` will make no further
491progress, so ``Wake`` ing it serves no purpose.
492
493Once all ``Waker`` s have been cleared and the ``Task`` has finished running
494on the ``Dispatcher``, the ``Dispatcher`` should call that ``Task`` s
495``Cleanup`` function so that the ``Task`` can free any associated dynamic
496resources. During this ``Cleanup`` function, no other resources of ``Task``
497may be accessed by the application author until the ``Task`` has been
498re-initialized. If the memory associated with the ``Task`` is to be reused,
499the ``Task`` object itself must be reinitialized by invoking the ``Init``
500function.
501
502.. code-block:: cpp
503
504   class Task {
505    public:
506     ...
507     void Init();
508     virtual void Cleanup();
509     ...
510   };
511
512This allows downstream ``Task`` inheritors to implement dynamic free-ing of
513``Task`` resources, while also allowing the ``Dispatcher`` implementation the
514opportunity to clean up its own resources stored inside of the ``Task`` base
515class.
516
517Waker
518=====
519``Waker`` s will at first only be created via the ``Dispatcher``
520implementation, via cloning, or by the null constructor. Later on, the API may
521be expanded to allow for waking sub-tasks. The necessity of this at Pigweed's
522scale has not yet been determined.
523
524Timer
525=====
526``pw::async`` will additionally provide a ``Timer`` type. A ``Timer`` can be
527``Poll``'d by a ``Task`` in order to determine if a certain amount of time has
528passed. This can be used to implement timeouts or to schedule work.
529
530One possible ``Timer`` API would be as follows:
531
532.. code-block:: cpp
533
534   class Timer {
535    public:
536     Timer(Context&, chrono::SystemClock::time_point deadline);
537     Timer(Context&, chrono::SystemClock::duration delay);
538     pw::MaybeReady<Complete> Poll(Context&);
539     ...
540   };
541
542In order to enable this, the ``Dispatcher`` base class will include the
543following functions which implementations should use to trigger timers:
544
545.. code-block:: cpp
546
547   class DispatcherBase {
548    public:
549     ...
550    protected:
551     /// Returns the time of the earliest timer currently scheduled to fire.
552     std::optional<chrono::SystemClock::time_point> EarliestTimerExpiry();
553
554     /// Marks all ``Timer`` s with a time before ``time_point`` as complete,
555     /// and awakens any associated tasks.
556     ///
557     /// Returns whether any ``Timer`` objects were marked complete.
558     bool AwakenTimersUpTo(chrono::SystemClock::time_point);
559
560     /// Invoked when a new earliest ``Timer`` is created.
561     ///
562     /// ``Dispatcher`` implementations can override this to receive
563     /// notifications when a new timer is added.
564     virtual void NewEarliestTimer();
565     ...
566   };
567
568---------------------
569C++ Coroutine Support
570---------------------
571The informed ``Poll`` approach is well-suited to
572`C++20's coroutines <https://en.cppreference.com/w/cpp/language/coroutines>`_.
573Coroutines using the ``co_await`` and ``co_return`` expressions can
574automatically create and wait on ``Task`` types, whose base class will
575implement the ``std::coroutine_traits`` interface on C++20 and later.
576
577Dynamic Allocation
578==================
579Note that C++ coroutines allocate their state dynamically using
580``operator new``, and therefore are not usable on systems in which dynamic
581allocation is not available or where recovery from allocation failure is
582required.
583
584------------
585Rust Interop
586------------
587Rust uses a similar informed ``Poll`` model for its ``Future`` trait. This
588allows ``pw::async`` code to invoke Rust-based ``Future`` s by creating a
589Rust ``Waker`` which invokes the C++ ``Waker``, and performing cross-language
590``Poll`` ing.
591
592Rust support is not currently planned for the initial version of ``pw::async``,
593but will likely come in the future as Pigweed support for Rust expands.
594
595------------------------------------------------
596Support for Traditional Callback-Style Codebases
597------------------------------------------------
598One concern is interop with codebases which adopt a more traditional
599callback-driven design, such as the one currently supported by ``pw::async``.
600These models will continue to be supported under the new design, and can be
601modeled as a ``Task`` which runs a single function when ``Poll`` ed.
602
603---------
604Migration
605---------
606For ease of implementation and in order to ensure a smooth transition, this API
607will initially live alongside the current ``pw::async`` interface. This API
608will first be tested with one or more trial usages in order to stabilize the
609interface and ensure its suitability for Pigweed users.
610
611Following that, the previous ``pw::async`` implementation will be deprecated.
612A shim will be provided to allow users of the previous API to easily migrate
613their code onto the new ``pw::async`` implementation. After migrating to the
614new implementation, users can gradually transition to the new ``Poll``-based
615APIs as-desired. It will be possible to intermix legacy-style and
616``Poll``-based async code within the same dispatcher loop, allowing legacy
617codebases to adopt the ``Poll``-based model for new subsystems.
618