oxidizer/crates/thread_aware/README.md at main · microsoft/oxidizer · GitHub
Skip to content

Latest commit

 

History

History
162 lines (125 loc) · 9.33 KB

README.md

File metadata and controls

162 lines (125 loc) · 9.33 KB
Thread Aware Logo

Thread Aware

crate.io docs.rs MSRV CI Coverage License This crate was developed as part of the Oxidizer project

Essential building blocks for thread-per-core libraries.

Crate features

  • The std Cargo feature (enabled by default) enables the ThreadBuilder runtime integration API.
  • derive (default) re-exports the #[derive(ThreadAware)] macro.
  • Disable default features for #![no_std] environments. The core thread vocabulary, closures, and wrappers remain available. Enable derive explicitly if the derive macro is needed.

no_std environments require alloc and pointer-width atomics.

This crate re-exports every type from thread_aware_core, which is the authoritative stable relocation contract. It adds derive support, closures, wrappers, runtime thread construction, and runtime thread construction.

Theory of Operation

At a high level, this crate enables thread migrations of state via the ThreadAware trait:

  • Runtimes (and similar) can use it to inform types that they were just moved across a thread or NUMA boundary.
  • The authors of said types can then act on this information to implement performance optimizations. Such optimizations might include re-allocating memory in a new NUMA region, connecting to a thread-local I/O scheduler, or detaching from shared, possibly contended memory with the previous thread.

Similar to Clone, there are no exact semantic prescriptions of how types should behave on relocation. They might continue to share some state (e.g., a common cache) or fully detach from it for performance reasons. The primary goal is performance, so types should aim to minimize contention on synchronization primitives and cross-NUMA memory access. Like Clone, the relocation itself should be mostly transparent and predictable to users. Relocation is always a callback on the current thread: the runtime must move or schedule the value to the destination first, then invoke ThreadAware::relocate on that destination thread. The destination argument describes the thread executing the callback; it does not identify a remote thread for the callback to run on. This is important for implementations that release, allocate, or initialize thread- and NUMA-local state.

Implementing ThreadAware

In most cases ThreadAware should be implemented via the provided derive macro. As thread-awareness of a type usually involves letting all contained fields know of an ongoing relocation, the derive macro does just that. A default impl is provided for many std types, so the macro should ‘just work’ on most compounds of built-ins.

Relation to Send

ThreadAware requires Send as a supertrait. Types are first sent to the destination thread, then the ThreadAware relocation notification is invoked on that same thread.

Thread vs. Core Semantics

As this library is primarily intended for use in thread-per-core runtimes, we use the terms ‘thread’ and ‘core’ interchangeably. The assumption is that items primarily arrive on different threads, where each thread is pinned to a different CPU core. Should a runtime utilize more than one thread per core (e.g., for internal I/O) user code should be able to observe this fact.

Sometimes you might need to move inert types as-is, essentially bypassing all thread-aware handling. These might be foreign types that carry no allocation, do no I/O, or otherwise do not require any thread-specific handling.

Unaware can be used to encapsulate such types, a wrapper that itself implements ThreadAware, but otherwise does not react to it. You can think of it as a MoveAsIs<T>. However, it was deliberately named Unaware to signal that only types which are genuinely unaware of their thread relocations (i.e., don’t impl ThreadAware) should be wrapped in such.

Wrapping types that implement the trait is discouraged, as it will prevent them from properly relocating and might have an impact on their performance, but not correctness, see below.

Performance vs. Correctness

It is important to note that ThreadAware is a cooperative performance optimization and contention avoidance primitive, not a guarantee of behavior for either the caller or callee. In other words, callers and runtimes must continue to operate correctly if the trait is invoked incorrectly.

In particular, ThreadAware may not always be invoked when a type leaves the current thread. While runtimes should reduce the incidence of that through their API design, it may nonetheless happen via std::thread::spawn and other means. In these cases types should still function correctly, although they might experience degraded performance through contention of now-shared resources.

Provided Implementations

thread_aware_core implements ThreadAware for core, alloc, and standard library types. Implementations for third-party types live with those types once the stable trait can be adopted natively. Until then, inert foreign values can be wrapped in Unaware.

Features

  • The std Cargo feature (enabled by default) enables the ThreadBuilder runtime integration API. Disable it for #![no_std] environments; the crate then requires alloc and pointer-width atomics.
  • derive (default): Re-exports the #[derive(ThreadAware)] macro from the companion thread_aware_macros crate. Disable to avoid pulling in proc-macro code in minimal environments. For derive support without std, use default-features = false, features = ["derive"].

Examples

When the derive feature (enabled by default) is active you can simply use the ThreadAware derive macro instead of writing the implementation manually.

use thread_aware::ThreadAware;

#[derive(Debug, Clone, ThreadAware)]
struct Point {
    x: i32,
    y: i32,
}

This crate was developed as part of The Oxidizer Project. Browse this crate's source code.