memory-model
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Use when choosing or reviewing atomic orderings (Ordering::Relaxed, Acquire, Release, AcqRel, SeqCst) or declaring global state (static mut, const vs static, OnceLock, LazyLock, thread_local), including happens-before edges, fences, compare-exchange or fetch_update loops, lock-free code, and a data race seen only on ARM64. Triggers on "global variable", "lazy_static", "once_cell".
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-borrow-semanticsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-debuggingSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-observabilitySkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-send-syncSkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-test-toolsSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-varianceSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 84
Check each item before you approve a diff that touches atomics.
- Very specific: line 128
OnceLockandLazyLockdiffer on panic: When the initializer panics, OnceLock::get_or_init leaves the cell empty and the next call … - Very specific: line 142
Do not rewrite a static mut to an atomic without reading its type.
- Distinctive: line 226
Read ordering-patterns.md when you write or review a counter or unique id, publish/subscribe, a stop flag or progress counter, or a …
- Very specific: line 239
Prefer an ordered atomic operation over a separate fence, because the pairing of a fence is not visible at the access site.
- Very specific: line 247
Use these skills when they are installed: rust-send-sync for Send and Sync bounds before any ordering question, rust-async-internals for …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Memory Model
Assume a weakly ordered target (ARM64, RISC-V, POWER). These CPUs reorder memory accesses that x86 keeps in order. An ordering bug can pass every test on x86 and fail on a phone or on an Apple silicon Mac.
Core rules
- Derive the ordering from the data it publishes. Name the writes that the
reader must see, then pick the weakest ordering that publishes them. Do not
start from
SeqCst: it hides the missing reasoning, and it does not repair an unpaired or misplaced access. - Ordering is a property of a pair. A
Releasestore synchronizes with anAcquireload of the same atomic that reads the stored value. - An atomic that publishes nothing needs only
Relaxed. Counters and unique ids are the usual case. - Prefer a lock,
OnceLock, or a channel over hand-rolled publication. To publish non-atomic data through a flag you needUnsafeCellandunsafe. Treat that code as a lock-free structure: it needs a loom test and an unsafe review (therust-unsafeskill, when it is installed). - A pass on x86 proves nothing about ordering. The compiler also reorders
Relaxedaccesses, so aRelaxedbug can appear on x86 after an unrelated optimization change. Use the checks in Verification.
Common mistakes
| Mistake | Fix |
Relaxed used for publish/subscribe | Use Release on the store and Acquire on the load. |
SeqCst used everywhere "to be safe" | Derive the ordering from the data dependency. Keep SeqCst only for a named multi-atomic order. |
Release on one atomic paired with Acquire on a different atomic | Pair the operations on the same atomic, or build an explicit synchronization chain. A SeqCst fence alone does not connect different atomics. |
static mut used for shared data | Use Mutex, RwLock, OnceLock, LazyLock, or an atomic. |
const used for shared state with interior mutability | Declare it static. A const is inlined, so every use site mutates a fresh copy. |
| Non-atomic data guarded only by an atomic flag | Guard the payload with a lock. Hand-rolled UnsafeCell publication needs a loom test and an unsafe review. |
Relaxed decrement in a reference count | Use AcqRel, or Release plus an Acquire fence before the destructor. |
compare_exchange on a pointer or index that can be freed and reused (ABA) | Pack a generation counter with the value, or use epoch-based reclamation. |
Side effect in an update or try_update closure | Keep the closure free of side effects. It runs again after each lost race. |
compiler_fence used to order memory between threads | Use fence or an ordered atomic. compiler_fence emits no CPU barrier. |
| ThreadSanitizer reports a race on fence-based code | Do not strengthen the ordering to silence it. TSan does not support fence, so the report can be false. Check with Miri or loom. |
| ARM64 assumed to behave like x86 | Run Miri and loom, and test on a weakly ordered host. |
Verification
Match the check to the risk. Each check misses a class of bugs.
# Miri: data races and UB on the executed paths, one schedule per seed.
MIRIFLAGS="-Zmiri-many-seeds=0..16" cargo +nightly miri test --locked <test_filter>
# loom: all interleavings of a loom-instrumented test.
RUSTFLAGS="--cfg loom" cargo test --locked --release --test <loom_target>
# Hardware: the normal suite on an aarch64 host or device.
cargo test --locked- Miri runs, when a nightly toolchain with the
miricomponent is installed, for any change that adds or edits an atomic operation that a test reaches. Without it, report the atomic change as not checked by Miri. Each seed is one schedule, and Miri emulates only some weak memory effects, so a pass is evidence, not proof. Miri model flags and CI setup live in therust-sanitizers-miriskill, when it is installed. - loom runs for a lock-free structure, a hand-written lock, or publication
through
UnsafeCell. It checks only code built on its types:loom::syncatomics,loom::thread, andloom::cell::UnsafeCellfor the payload. A payload instd::cell::UnsafeCellis invisible to loom, so gate the cell type withcfg(loom)too. Setup lives in therust-test-toolsskill, when it is installed. Know its two limits. It treatsSeqCstloads and stores asAcqRel, so a failure inSeqCstcode can be a false alarm (fence(SeqCst)is modelled). It does not explore load buffering, so a pass is not proof. ALOOM_MAX_PREEMPTIONSbound also limits what a pass covers. - A weakly ordered host runs the suite before you ship a lock-free structure. An Apple silicon Mac qualifies. A finite run that does not fail proves only that this run did not fail.
Review checklist
Check each item before you approve a diff that touches atomics.
- Every
Releasestore has a matchingAcquireload of the same atomic. - Every ordering is the weakest one that is still correct, with a comment that names the data it publishes.
- No
SeqCstremains without a stated multi-atomic ordering requirement. - Non-atomic shared data is guarded by a lock, not by the flag alone.
- Reference-count decrements use
AcqRel, orReleaseplus anAcquirefence. - Every compare-exchange and
updatefailure ordering isRelaxed,Acquire, orSeqCst, and is chosen for what the losing thread reads. - Every CAS on a pointer or index is safe against ABA.
- New code uses
updateortry_update, notfetch_update, unless the MSRV is below 1.95. - Busy-wait loops call
std::hint::spin_loop()or are replaced by blocking. - The change has a Miri run, or the report says it is not checked by Miri
because no nightly
miricomponent is installed. A lock-free change also has a loom test.
Global state
A global is shared state, so every rule above applies to it. Choose the declaration form first. Each form fails in a different way.
| Form | Use it for | Cost |
const NAME: T | A compile-time value with no identity: a limit, a table of &str. | Inlined at every use site. No address. No state. |
static NAME: T | Shared state with a const initializer: an atomic, a Mutex. | One address for the process. T must be Sync. |
static NAME: OnceLock<T> | A value written once at start-up by its owner. | One atomic load per read. get returns Option<&T>. |
static NAME: LazyLock<T> | A value computed on first use from a fixed initializer. | One atomic load per read. Derefs to &T. |
thread_local! { static NAME: T } | Per-thread scratch: a buffer, a recursion depth, an RNG. | No synchronization. One value per thread. |
Five rules decide the choice. Each one is a compile error or a silent bug.
- A
staticneedsSync.static BAD: RefCell<u32>is rejected witherror[E0277]: RefCell<u32> cannot be shared between threads safelyand the noteshared static variables must have a type that implements Sync. Put the value in aMutexor anRwLock, or use an atomic. - A
consthas no single address. The compiler inlines the value at every use site, so interior mutability in aconstmutates a fresh temporary and discards it.const HITS: AtomicUsizeincremented three times reads back0. The same declaration as astaticreads back3. rustc 1.93 and later warns withconst_item_interior_mutations, and clippy warns withclippy::declare_interior_mutable_const. Neither is an error, so this ships. OnceLockandLazyLockdiffer on panic. When the initializer panics,OnceLock::get_or_initleaves the cell empty and the next call retries.LazyLockpoisons for the life of the process: every later deref panics withLazyLock instance has previously been poisoned. UseLazyLockfor an initializer that cannot fail. UseOnceLockwhen the value arrives from outsidemain, or when the first attempt can fail.- A
staticis never dropped. Process exit runs noDropon astatic, so a flush-on-drop writer in a global loses its last buffer. Flush it explicitly beforemainreturns. thread_local!destructors have holes. A destructor runs when its thread exits, but std documents that on Unix with pthread-based TLS it does not run for a value on the main thread. Never put required cleanup in thread-local state.
Do not rewrite a static mut to an atomic without reading its type. An atomic
replaces a static mut that holds an integer or a bool. A static mut that
holds a Vec or a String needs OnceLock or LazyLock plus a Mutex. On
edition 2024 the static_mut_refs lint is deny-by-default, so every reference
to the static stops the build. The cargo-workflows skill, when it is
installed, has the two exact messages and the edition-2024 migration steps.
LazyLock is stable since 1.80.0 and OnceLock since 1.70.0. A workspace on a
newer toolchain needs no lazy_static or static_init dependency. Keep
once_cell while rust-version stays below 1.80.0, or while a global
initializer returns a Result. OnceLock::get_or_try_init is still unstable on
1.98.1. It fails with error[E0658] under the feature gate once_cell_try,
issue 109737.
use std::cell::Cell;
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex, OnceLock};
// One address, computed on first deref from a fixed initializer.
static TABLE: LazyLock<Mutex<HashMap<u32, u64>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
// One address, written once at start-up by the owner of the value.
static CONFIG: OnceLock<String> = OnceLock::new();
// One value per thread. The `const` block skips the lazy-init check per access.
thread_local! {
static DEPTH: Cell<u32> = const { Cell::new(0) };
}
fn main() {
CONFIG.set(String::from("prod")).expect("set exactly once");
TABLE.lock().expect("TABLE poisoned").insert(1, 42);
DEPTH.set(DEPTH.get() + 1);
let len = TABLE.lock().expect("TABLE poisoned").len();
println!("{:?} {} {}", CONFIG.get(), len, DEPTH.get());
}Orderings
| Ordering | Valid on | Guarantee |
Relaxed | load, store, read-modify-write | Atomicity and one modification order per atomic. No order for other memory. |
Acquire | load, read-modify-write, fence, compiler_fence | When it reads the value of a Release store, every write before that store is visible after it. |
Release | store, read-modify-write, fence, compiler_fence | Publishes every earlier write to an Acquire load that reads this value. |
AcqRel | read-modify-write, fence, compiler_fence | Acquire on the read half, Release on the write half. |
SeqCst | all | Acquire and release, plus one total order over all SeqCst operations. |
An invalid ordering panics at run time: a load with Release or AcqRel, a
store with Acquire or AcqRel, a fence or compiler_fence with
Relaxed, and a compare-exchange or update failure ordering of Release or
AcqRel. The rustc lint invalid_atomic_ordering is deny-by-default and
rejects these calls at compile time when the ordering is a literal. It cannot
see an ordering that arrives through a variable, so the panic remains the last
line of defence.
Choosing an ordering
Use case Ordering
Counter, unique id, or statistic Relaxed
Stop flag or progress counter; reader uses nothing else Relaxed
Stop flag or progress counter; reader then reads state Release store, Acquire load
Publish data from one thread to another Release store, Acquire load
Read-modify-write that both consumes and publishes AcqRel
Reference count Relaxed increment;
Release or AcqRel decrement;
Acquire (load or fence) before the destructor
Lock word of a hand-written mutex or spinlock Acquire on the lock CAS (Relaxed on failure);
Release on unlock
Global order across several different atomics SeqCst on every participating accessThe lock row matches the std futex mutex:
compare_exchange(UNLOCKED, LOCKED, Acquire, Relaxed) to lock and
swap(UNLOCKED, Release) to unlock. SeqCst is needed only when correctness
depends on two different atomics being seen in one global order. Read
references/platform-memory-models.md
when you must justify SeqCst with a Dekker-style argument, port C++ atomics,
need the formal happens-before rules, or need the per-target barrier and
compare-exchange cost.
Read ordering-patterns.md when you write or review a counter or unique id, publish/subscribe, a stop flag or progress counter, or a reference count. It holds one worked example per pattern.
Compare-exchange and update loops
Read ordering-patterns.md
when you write or review compare_exchange, compare_exchange_weak, update, try_update, or
fetch_update: it holds the failure-ordering rules, the weak or strong choice, and the
fetch_update rename.
Fences
Prefer an ordered atomic operation over a separate fence, because the pairing
of a fence is not visible at the access site. A fence synchronizes only through
an atomic access. Read
references/platform-memory-models.md
when you write or review a fence or compiler_fence.
Related skills
Use these skills when they are installed: rust-send-sync for Send and Sync
bounds before any ordering question, rust-async-internals for tokio task,
cancellation, and runtime behavior, and rust-performance to measure the
barrier cost before you keep a stronger ordering.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

