-
Notifications
You must be signed in to change notification settings - Fork 29
docs(thread_aware): add a thread-aware authoring guide #742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1f928f5
3cd9d33
8e3237d
4a65451
840cede
cec7a5e
6033024
5fddfd7
550a1fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! A guide to authoring thread-aware types. | ||
| //! | ||
| //! The crate-level docs explain *what* [`ThreadAware`](crate::ThreadAware) is and the relocation | ||
| //! contract it expresses. This guide is the companion *how-to*: how to make your own types | ||
| //! thread-aware correctly, which implementation to reach for, how to test and debug the result, | ||
| //! and the mistakes that compile cleanly yet quietly do nothing. | ||
| //! | ||
| //! It is written for authors who see `T: ThreadAware` in an API and need to satisfy it, and for | ||
| //! reviewers deciding whether a `#[derive(ThreadAware)]` or a `#[thread_aware(skip)]` is the right | ||
| //! call. The lessons in [Anti-patterns](#anti-patterns) are drawn from migrating a large | ||
| //! production service onto an Oxidizer-backed runtime. | ||
| //! | ||
| //! # Why thread-awareness exists | ||
| //! | ||
| //! The crate-level [Theory of Operation](crate#theory-of-operation) covers what relocation is and | ||
| //! why thread-per-core runtimes need it. The one idea this guide leans on: relocation is a | ||
| //! **performance cooperation, never a correctness guarantee** (see | ||
| //! [Performance vs. Correctness](crate#performance-vs-correctness)). Nothing enforces it, so the | ||
| //! failure mode to design against is a type that *silently* fails to relocate. | ||
| //! | ||
| //! # Authoring a thread-aware type | ||
| //! | ||
| //! ## Prefer the derive | ||
| //! | ||
| //! In almost all cases, implement [`ThreadAware`](crate::ThreadAware) with | ||
| //! [the derive macro](https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html). It | ||
| //! generates a [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every | ||
| //! field, which is exactly what a compound type owes its parts: | ||
| //! | ||
| //! ```rust | ||
| //! use thread_aware::{Thread, ThreadAware}; | ||
| //! | ||
| //! #[derive(ThreadAware)] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 180583e. Rather than shim each doctest, I gated the whole module on the |
||
| //! struct Connection { | ||
| //! pool: Vec<u8>, | ||
| //! scratch: String, | ||
| //! } | ||
| //! | ||
| //! // A runtime hands `relocate` the worker the value came from and the one it is moving to. | ||
| //! fn on_move(mut c: Connection, from: Option<&Thread>, to: &Thread) { | ||
| //! c.relocate(from, to); | ||
| //! } | ||
| //! ``` | ||
| //! | ||
| //! The `std` library types you are most likely to hold - `Vec`, `Box`, `Option`, `Result`, tuples, | ||
| //! arrays, maps - already implement the trait, so the derive "just works" on compounds of them. | ||
| //! | ||
| //! ## Skipping a field | ||
| //! | ||
| //! Annotate a field with `#[thread_aware(skip)]` when it carries no affinity and should be moved | ||
| //! as-is: a plain identifier, a length, a foreign handle that does no thread-local work. A skipped | ||
| //! field is never relocated, and the derive adds a `where Self: Send` bound to keep the | ||
| //! `ThreadAware: Send` supertrait satisfied. | ||
| //! | ||
| //! ```rust | ||
| //! use thread_aware::ThreadAware; | ||
| //! | ||
| //! #[derive(ThreadAware)] | ||
| //! struct Request { | ||
| //! body: Vec<u8>, | ||
| //! // A request id has no thread affinity; moving it verbatim is correct. | ||
| //! #[thread_aware(skip)] | ||
| //! id: u64, | ||
| //! } | ||
| //! ``` | ||
| //! | ||
| //! `skip` is a claim that a field genuinely has nothing to rebind. It is not an escape hatch for | ||
| //! "this field does not implement `ThreadAware` yet" - reach for | ||
| //! [`Unaware`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) or the | ||
| //! strategy-partitioned `Arc` (with the `std` feature) for that, so the intent is visible in the | ||
| //! type. | ||
| //! | ||
| //! ## What the generated bounds mean | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this might be too much detail, I myself had trouble grasping what is this traying to say
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed - cut it down to a short pointer at the derive's Generic Bounds reference (53ca92f). |
||
| //! | ||
| //! You rarely need to reason about this: the derive adds exactly the `ThreadAware` bounds its | ||
| //! generated body needs and no more, so a correct type "just derives". When it matters - a generic | ||
| //! wrapper, or a marker field that should stay bound-free - the derive's | ||
| //! [Generic Bounds](https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html#generic-bounds) | ||
| //! reference has the rules. | ||
| //! | ||
| //! ## Implementing the trait by hand | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am missing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a "Per-worker state with |
||
| //! | ||
| //! Write the impl yourself when relocation means something specific - re-homing an allocation, | ||
| //! swapping a per-core cache, reconnecting to a scheduler. The method receives the source worker | ||
| //! (`None` if unknown) and the destination: | ||
| //! | ||
| //! ```rust | ||
| //! use thread_aware::{Thread, ThreadAware}; | ||
| //! | ||
| //! struct PerCoreScratch { | ||
| //! buffer: Vec<u8>, | ||
| //! } | ||
| //! | ||
| //! impl ThreadAware for PerCoreScratch { | ||
| //! fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) { | ||
| //! // The scratch buffer belonged to the previous worker; drop it so the next use | ||
| //! // re-allocates in the destination's NUMA node instead of reaching across. | ||
| //! self.buffer = Vec::new(); | ||
|
Comment on lines
+99
to
+101
|
||
| //! } | ||
| //! } | ||
| //! ``` | ||
| //! | ||
| //! ## Per-worker state with `Arc` | ||
| //! | ||
| //! When several workers share a value but each should keep its *own* instance - a per-core cache, a | ||
| //! pool you do not want contended across cores - wrap it in the strategy-partitioned `Arc<T, S>` | ||
| //! ([`thread_aware::Arc`](https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html), with | ||
| //! the crate's `std` feature). With the `PerThread` strategy, | ||
| //! relocation materializes a separate `T` for the destination worker (lazily, on first use there), | ||
| //! so the sharing is per-worker instead of process-wide. Use `PerProcess`, which behaves as a | ||
| //! vanilla `Arc`, when one shared instance is what you want, and `PerNumaNode` for one instance per | ||
| //! NUMA node. This is also the usual bridge to a type that does not implement `ThreadAware` itself. | ||
| //! | ||
| //! # Choosing an implementation | ||
| //! | ||
| //! | You have… | Reach for | Because | | ||
| //! |---|---|---| | ||
| //! | A compound of thread-aware fields | `#[derive(ThreadAware)]` | Forwards relocation to each field. | | ||
| //! | A field with genuine per-core behavior | a hand-written impl | Only you know what "rebind" means. | | ||
| //! | A foreign type that carries no affinity | [`Unaware<T>`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) | A `MoveAsIs<T>`: implements the trait as a no-op. | | ||
| //! | Shared state that should differ per worker | `Arc<T, PerThread>` (`std`) | Materializes a separate `T` per destination. | | ||
| //! | Shared state that is the same everywhere | `Arc<T, PerProcess>` (`std`) | Behaves as a vanilla `Arc`. | | ||
| //! | ||
| //! [`Unaware`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) wraps a value | ||
| //! and satisfies `ThreadAware` without reacting to | ||
| //! relocation - use it for inert, foreign, or allocation-free values that legitimately do not care | ||
| //! which worker they are on. Wrapping a type that *does* implement the trait is discouraged: it | ||
| //! silences that type's own relocation (a performance loss, not a correctness bug). | ||
| //! | ||
| //! # Anti-patterns | ||
| //! | ||
| //! These are the shapes that compile, satisfy `T: ThreadAware`, and still leave state stranded on | ||
| //! the wrong worker. None of them produces a compile error, and most produce no runtime warning | ||
| //! either, so they are worth recognizing by sight. | ||
| //! | ||
| //! ## `Clone` does not relocate | ||
| //! | ||
| //! This is the one to internalize first. A thread-aware type stores its affinity in a field that | ||
| //! only [`relocate`](crate::ThreadAware::relocate) mutates. **`Clone` copies that stored affinity | ||
| //! verbatim.** Cloning a value that was built on worker A and using the clone on worker B does not | ||
| //! move it to B - it is still bound to A, quietly, until something calls `relocate`. | ||
| //! | ||
| //! ```text | ||
| //! let services = build_on_startup_worker(); // affinity = startup worker | ||
| //! let per_request = services.clone(); // affinity = startup worker (copied!) | ||
| //! // `per_request` now funnels every task back onto the startup worker. | ||
| //! ``` | ||
| //! | ||
| //! In one migration this single clone routed an entire process's work onto one core while the | ||
| //! others idled. If you clone a long-lived, affinity-bearing graph, relocate the clone at the point | ||
| //! it enters its new worker. | ||
| //! | ||
| //! ## `skip` on the only field is a silent no-op | ||
| //! | ||
| //! `#[thread_aware(skip)]` on the *sole* field of a type makes `relocate` do nothing, yet the type | ||
| //! still satisfies `T: ThreadAware`. Downstream code compiles, runtimes accept it, and no affinity | ||
| //! ever moves - with no error and no warning. A type whose every field is skipped is | ||
| //! indistinguishable from one that is genuinely inert; make sure that is what you meant. | ||
| //! | ||
| //! ## Do not trust inherited markings | ||
| //! | ||
| //! An existing `#[derive(ThreadAware)]` or `#[thread_aware(skip)]` is a decision someone made under | ||
| //! their constraints, and at least one such marking per audit tends to be a compile-shortcut that | ||
| //! reduces to a silent no-op. When you take a dependency on a type being thread-aware, verify that | ||
| //! its relocation actually reaches the state you care about rather than inheriting the annotation as | ||
| //! fact. | ||
| //! | ||
| //! ## Relocate the whole graph once, at the boundary | ||
| //! | ||
| //! When work crosses into a worker from the outside - an FFI entry, a hand-off from a foreign | ||
| //! thread - relocate the entire long-lived dependency graph **once**, at that boundary, rather than | ||
| //! special-casing each affinity-bearing dependency downstream. Make the graph's root `ThreadAware` | ||
| //! (by derive) so a single `relocate` at the entry rebinds every affinity-bearing resource beneath | ||
| //! it. | ||
| //! Relocating a subtree while its parent was built from a stale clone (see above) is how affinity | ||
| //! goes stale in practice. | ||
| //! | ||
| //! # Testing | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check thread_aware::Relocator (under "test-util") that could be used for relocation testing. Also too much detail too, make it more concise.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made Testing more concise and now lead with the |
||
| //! | ||
| //! Relocation is silent when it is wrong, so test it by observation. Compose a leaf type whose | ||
| //! `relocate` records that it ran, relocate the type under test once, and assert that every | ||
| //! non-skipped field was reached and every skipped one was not: | ||
| //! | ||
| //! ```rust | ||
| //! use thread_aware::{Thread, ThreadAware}; | ||
| //! | ||
| //! /// Counts relocations so a test can prove which fields the derive reaches. | ||
| //! #[derive(Default)] | ||
| //! struct Tracker { | ||
| //! relocations: usize, | ||
| //! } | ||
| //! | ||
| //! impl ThreadAware for Tracker { | ||
| //! fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) { | ||
| //! self.relocations += 1; | ||
| //! } | ||
| //! } | ||
| //! | ||
| //! #[derive(ThreadAware)] | ||
| //! struct UnderTest { | ||
| //! tracked: Tracker, | ||
| //! #[thread_aware(skip)] | ||
| //! skipped: Tracker, | ||
| //! } | ||
| //! | ||
| //! fn assert_reaches_the_right_fields(from: Option<&Thread>, to: &Thread) { | ||
| //! let mut value = UnderTest { | ||
| //! tracked: Tracker::default(), | ||
| //! skipped: Tracker::default(), | ||
| //! }; | ||
| //! value.relocate(from, to); | ||
| //! assert_eq!( | ||
| //! value.tracked.relocations, 1, | ||
| //! "non-skipped fields must be relocated" | ||
| //! ); | ||
| //! assert_eq!( | ||
| //! value.skipped.relocations, 0, | ||
| //! "skipped fields must not be relocated" | ||
| //! ); | ||
| //! } | ||
| //! ``` | ||
| //! | ||
| //! The `test-utils` feature adds a | ||
| //! [`Relocator`](https://docs.rs/thread_aware/latest/thread_aware/struct.Relocator.html) that drives | ||
| //! relocations without hand-built [`Thread`](crate::Thread) values, which is usually what a real | ||
| //! test wants. | ||
| //! | ||
| //! # Validating correctness | ||
| //! | ||
| //! What the toolchain checks for you, and what it cannot: | ||
| //! | ||
| //! * **The compiler** enforces the `ThreadAware: Send` supertrait and, through the derive's | ||
| //! field-type bounds, that every relocated field is itself thread-aware. It cannot tell whether a | ||
| //! `#[thread_aware(skip)]` is *justified* - only that the resulting type is still `Send`. | ||
| //! * **The derive** emits each field-type predicate once and suppresses a bound the author already | ||
| //! wrote, so a correct `#[derive(ThreadAware)]` does not trip | ||
| //! `clippy::trait_duplication_in_bounds`. | ||
| //! * **Your tests** are the only thing that checks the property that actually matters: that | ||
| //! relocation reaches the state it is supposed to. Nothing else does. | ||
| //! | ||
| //! The through-line of this guide: a thread-aware type that does the wrong thing usually does it | ||
| //! silently. Author for that - prefer the derive, justify every `skip`, relocate clones and graphs | ||
| //! at their boundaries, and prove it with a test that observes relocation happening. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,6 +133,15 @@ extern crate std; | |
|
|
||
| mod wrappers; | ||
|
|
||
| /// A guide to authoring thread-aware types: how to implement, test, and debug them, and the | ||
| /// anti-patterns to avoid. See [the guide](_documentation). | ||
| /// | ||
| /// Gated on `derive` because every example is built around `#[derive(ThreadAware)]`, which is only | ||
| /// available with that feature; this keeps the guide's examples valid in a build without it (they | ||
| /// are simply absent) per `docs/feature-gated-doctests.md`. | ||
| #[cfg(all(any(doc, test), feature = "derive"))] | ||
| pub mod _documentation; | ||
|
Copilot marked this conversation as resolved.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! I've gated it with On the other two points:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gated with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update on the "move to That removes the blocker I raised earlier: the guide no longer needs any of those types in scope, so it could move to Happy to move the module to |
||
|
|
||
| pub mod closure; | ||
|
|
||
| #[cfg(feature = "test-utils")] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should just link to main lib.rs docs, too much duplication
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trimmed to a pointer at the crate-level Theory of Operation (53ca92f); dropped the duplication.