This is very WIP and also very vibe-coded.
Async patterns built on poll, Pin and Waker, explained.
async fn is a higher level interface for writing async code. But some things cannot be written that way: a future you need to name, so it can sit in a struct field or have traits implemented on it; a leaf future woken by something outside the async world; a combinator driving several futures at once; or a type whose behaviour after completion is part of its contract.
Doing it by hand sometimes means satisfying compiler-enforced invariants which are difficult to meet, or upholding contracts the compiler does not check.
This library contains a set of examples to illustrate lower level async concepts, and show how to write code which satisfies both the compiler and those other invariants.
The examples are organised by complexity:
Start here to understand the fundamentals of the Future trait:
- [
foundations::poll::ready] - A future that immediately returns a value. - [
foundations::poll::pending] - A future that never completes. - [
foundations::yield_now] - A future that gives up the thread once. - [
foundations::wrapper] - Wrap an existing future in a newtype.
These demonstrate the basic structure of futures and introduce concepts like polling, wakers, and pinning.
Learn how to build futures with internal state transitions:
- [
state_machine::maybe_done] - Track whether a future has completed. - [
state_machine::two_state] - Simple countdown state machine.
State machines are fundamental to implementing complex async behaviour. These examples show how to use enums to represent different states and manage transitions during polling.
Where readiness comes from in the first place:
- [
waking::shared_state] - A future woken by another thread.
Every other example here completes immediately, wakes itself, or forwards a poll to a future underneath it. This one parks and is woken by something external, which is what leaf futures do and what the rest are ultimately built on.
Build futures that drive other futures:
- [
composition::map] - Transform a future's output. - [
composition::race] - Return the first of two futures to complete. - [
composition::join] - Wait for two futures and collect both outputs. - [
composition::try_join] - The same, but stop at the first error. - [
composition::option] - A future that may or may not be there. - [
composition::fuse] - Make polling after completion harmless.
Composition is key to building complex async operations from simple pieces. These examples introduce pin projection and coordinating multiple futures.
Work with time and deadlines using tokio's timer infrastructure:
- [
time::timeout] - Require a future to complete within a time limit.
This demonstrates integration with runtime services and practical patterns for real-world async code.
- [
pinning] -Pin,Unpin, and structural pinning. - [
pinning::pin_project] - Whatpin_project!generates, and what it refuses.
Everything that holds another future needs this; everything that does not, does not. It sits between the two in the learning path.
Deeper explorations into more advanced topics:
- [
advanced::poll_fn] - Wrap a closure into a future.
- [
testing] - Poll futures by hand, and count wakes.
.await only reveals a future's final output. These helpers drive a future one poll at a time so tests can assert on the whole poll sequence -- how many polls it took, and whether the task was woken when it should have been. That is where the subtle bugs in a Future impl actually live.
Recommended order for learning:
- [
foundations::poll::ready] and [foundations::poll::pending] - always ready, and never ready. - [
foundations::yield_now] - pending once, and arranging its own wake. - [
foundations::wrapper] - wrapping another future, and the two routes to the future inside. - [
state_machine::two_state] - states written out by hand, and asking to be polled again. - [
waking::shared_state] - a future that waits until another thread wakes it. Where readiness comes from. - [
pinning] - the choice each wrapper makes about the field it holds, and what it commits to. - [
pinning::pin_project] - the macro's guarantees: theUnpinimpl it writes, and the impls it refuses. - [
state_machine::maybe_done] - keeping a finished future's output while the others catch up. - [
composition::map] - transforming another future's output. - [
composition::race] - whichever of two finishes first, and why the polling order matters. - [
composition::join] - waiting for both. Then [composition::try_join], where failing early means abandoning a branch. - [
composition::option] - an optional step, and pinning through a container. - [
composition::fuse] - promising more than theFuturecontract requires, and [fused] for saying so. - [
time::timeout] - racing against the runtime's timer. The first example that needs a runtime. - [
advanced::poll_fn] - a future from a closure. The first place pinning forcesunsafe.
[testing] is useful throughout; reach for it as soon as you want to assert on something .await cannot show you.
Run cargo doc --open to view the full documentation, including the key concepts the examples share.
Run cargo test to check them. Most of the explanation lives in doc comments, so the doctests are a substantial part of the suite; the poll-level tests that .await cannot express sit beside the code they cover, in each module's mod tests.
Source read while writing this. Where a module is derived from one of them rather than merely informed by it, NOTICE.md says so.
Explanations of the machinery itself, which cover it better than a paraphrase here could:
core::pin—Pin,Unpin, and its "Projections and Structural Pinning" section.pin_project!— the macro's full surface, feature by feature.- The Rust async book
MIT — see LICENSE.
Some modules are derived from tokio, futures-rs and the Rust standard library, all used here under MIT. Their notices, and which file each follows, are in NOTICE.md.
0 comments
log in to comment.