agentleFS
po4ykapo4yka/rust-skills/skills/memory-model/SKILL.md

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".

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
  1. Distinctive:

    Check each item before you approve a diff that touches atomics.

    line 84
  2. Very specific:

    OnceLock and LazyLock differ on panic: When the initializer panics, OnceLock::get_or_init leaves the cell empty and the next call …

    line 128
  3. Very specific:

    Do not rewrite a static mut to an atomic without reading its type.

    line 142
  4. Distinctive:

    Read ordering-patterns.md when you write or review a counter or unique id, publish/subscribe, a stop flag or progress counter, or a …

    line 226
  5. Very specific:

    Prefer an ordered atomic operation over a separate fence, because the pairing of a fence is not visible at the access site.

    line 239
  6. Very specific:

    Use these skills when they are installed: rust-send-sync for Send and Sync bounds before any ordering question, rust-async-internals for …

    line 247

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  1. 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.
  2. Ordering is a property of a pair. A Release store synchronizes with an Acquire load of the same atomic that reads the stored value.
  3. An atomic that publishes nothing needs only Relaxed. Counters and unique ids are the usual case.
  4. Prefer a lock, OnceLock, or a channel over hand-rolled publication. To publish non-atomic data through a flag you need UnsafeCell and unsafe. Treat that code as a lock-free structure: it needs a loom test and an unsafe review (the rust-unsafe skill, when it is installed).
  5. A pass on x86 proves nothing about ordering. The compiler also reorders Relaxed accesses, so a Relaxed bug can appear on x86 after an unrelated optimization change. Use the checks in Verification.

Common mistakes

MistakeFix
Relaxed used for publish/subscribeUse 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 atomicPair 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 dataUse Mutex, RwLock, OnceLock, LazyLock, or an atomic.
const used for shared state with interior mutabilityDeclare it static. A const is inlined, so every use site mutates a fresh copy.
Non-atomic data guarded only by an atomic flagGuard the payload with a lock. Hand-rolled UnsafeCell publication needs a loom test and an unsafe review.
Relaxed decrement in a reference countUse 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 closureKeep the closure free of side effects. It runs again after each lost race.
compiler_fence used to order memory between threadsUse fence or an ordered atomic. compiler_fence emits no CPU barrier.
ThreadSanitizer reports a race on fence-based codeDo 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 x86Run 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 miri component 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 the rust-sanitizers-miri skill, 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::sync atomics, loom::thread, and loom::cell::UnsafeCell for the payload. A payload in std::cell::UnsafeCell is invisible to loom, so gate the cell type with cfg(loom) too. Setup lives in the rust-test-tools skill, when it is installed. Know its two limits. It treats SeqCst loads and stores as AcqRel, so a failure in SeqCst code can be a false alarm (fence(SeqCst) is modelled). It does not explore load buffering, so a pass is not proof. A LOOM_MAX_PREEMPTIONS bound 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 Release store has a matching Acquire load of the same atomic.
  • Every ordering is the weakest one that is still correct, with a comment that names the data it publishes.
  • No SeqCst remains 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, or Release plus an Acquire fence.
  • Every compare-exchange and update failure ordering is Relaxed, Acquire, or SeqCst, and is chosen for what the losing thread reads.
  • Every CAS on a pointer or index is safe against ABA.
  • New code uses update or try_update, not fetch_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 miri component 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.

FormUse it forCost
const NAME: TA compile-time value with no identity: a limit, a table of &str.Inlined at every use site. No address. No state.
static NAME: TShared 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.

  1. A static needs Sync. static BAD: RefCell<u32> is rejected with error[E0277]: RefCell<u32> cannot be shared between threads safely and the note shared static variables must have a type that implements Sync. Put the value in a Mutex or an RwLock, or use an atomic.
  2. A const has no single address. The compiler inlines the value at every use site, so interior mutability in a const mutates a fresh temporary and discards it. const HITS: AtomicUsize incremented three times reads back 0. The same declaration as a static reads back 3. rustc 1.93 and later warns with const_item_interior_mutations, and clippy warns with clippy::declare_interior_mutable_const. Neither is an error, so this ships.
  3. OnceLock and LazyLock differ on panic. When the initializer panics, OnceLock::get_or_init leaves the cell empty and the next call retries. LazyLock poisons for the life of the process: every later deref panics with LazyLock instance has previously been poisoned. Use LazyLock for an initializer that cannot fail. Use OnceLock when the value arrives from outside main, or when the first attempt can fail.
  4. A static is never dropped. Process exit runs no Drop on a static, so a flush-on-drop writer in a global loses its last buffer. Flush it explicitly before main returns.
  5. 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

OrderingValid onGuarantee
Relaxedload, store, read-modify-writeAtomicity and one modification order per atomic. No order for other memory.
Acquireload, read-modify-write, fence, compiler_fenceWhen it reads the value of a Release store, every write before that store is visible after it.
Releasestore, read-modify-write, fence, compiler_fencePublishes every earlier write to an Acquire load that reads this value.
AcqRelread-modify-write, fence, compiler_fenceAcquire on the read half, Release on the write half.
SeqCstallAcquire 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 access

The 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.