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