Skip to content
← All articles

What this grader cannot check, and what real teams use

A green tick here means "produced the right answer on this machine, this time". An honest inventory of the concurrency bugs that are invisible to this course's tooling, a scorecard for clippy, and the three tools that actually find them.

You have now written two dozen concurrent programs and every one of them passed. This page is about what that does and does not mean, because a course that pretends its grader is a correctness oracle produces overconfident engineers, and in concurrent code that is the most dangerous kind.

What a passing tick actually certifies

This program compiled, satisfied clippy -D warnings, and produced the expected output on this machine, on this run.

That is the entire claim. It is a real claim — deterministic assertions over genuinely racy executions caught a great many mistakes along the way — and it stops well short of “this program is correct”.

The bugs that are invisible here

Memory-ordering mistakes. Weaken the Release in the SPSC ring buffer to Relaxed and every test still passes, because on x86-64 the two compile to the same instruction. On aarch64 the program becomes wrong occasionally. Nothing in a single-file rustc -O build can distinguish a correct ordering from a lucky one.

The ABA problem. A CAS loop reads A, another thread changes it to B and back to A, and your compare_exchange succeeds against a value that is numerically identical and semantically different. Lock-free stacks lose nodes this way. It needs a specific interleaving, and this grader runs one interleaving — whichever the OS picked.

A wrong unsafe impl Send. Get the bound wrong on your wrapper and an Rc escapes to another thread. The refcount races, the value is freed while live, and the program keeps running on freed memory. There is a decent chance it prints the right answer while doing it.

Almost all lock-free unsoundness, for the same reason: the failure needs a window of a few instructions to line up, and it will not on the run that counts.

Lock ordering. The transfers problem hung reliably — but only because it was designed to, with ten thousand crossing transfers. Real deadlocks appear under production load and not in tests.

The drop(tx) hang. Compiles clean, passes clippy clean, hangs forever. Zero diagnostics from anything.

An honest scorecard for clippy

From this course’s own testing, on clippy 0.1.95:

What it caught:

  • manual_div_ceil — an overflow bug dressed as a style nit.
  • let_underscore_lock — deny-by-default in rustc itself, a genuine hard error on a lock that guards nothing.
  • missing_spin_loop — but only when the atomic is a local; it did not fire through an Arc or a reference parameter.
  • rc_clone_in_vec_init, declare_interior_mutable_const and rustc’s const_item_interior_mutations — the const versus static mistake, flagged three separate ways.
  • invalid_atomic_ordering — deny-by-default, catching the syntactically impossible orderings for free.

What it caught nothing for:

  • the drop(tx) hang;
  • the lock-ordering deadlock;
  • a wrong memory ordering;
  • holding a guard across expensive work;
  • a spin loop that serialises a whole worker pool.

Do not oversell the tool. Clippy is excellent at local, syntactic, known mistakes and has essentially nothing to say about global, temporal, emergent ones. The bugs in the second list are the expensive ones.

The three tools that do find them

Miri — an interpreter for Rust’s MIR that executes your program under a model of the abstract machine rather than on hardware. It detects undefined behaviour: out-of-bounds accesses, use-after-free, invalid values, violations of the aliasing model (Stacked/Tree Borrows) — and, with -Zmiri-preemption-rate, data races. It is how you check an unsafe impl Send or a hand-written UnsafeCell wrapper.

rustup +nightly component add miri
cargo +nightly miri test

Needs nightly and cargo, so it cannot run here. It is also slow — orders of magnitude — which is fine, because you point it at the unsafe core, not at the whole program.

loom — exhaustive interleaving exploration under the C11 relaxed memory model. You write your test against loom::sync::atomic instead of std::sync::atomic, and loom runs it for every legal interleaving and every legal ordering outcome, including ones your hardware would never produce.

loom::model(|| {
    // your lock-free structure, checked under every schedule
});

This is the tool that would catch a wrong Release in the ring buffer. It is an external crate, so it cannot exist in a no-dependency single-file build. It also needs a small state space — you shrink the test to two threads and three operations, and that is the skill.

ThreadSanitizerRUSTFLAGS="-Zsanitizer=thread", nightly only. Instruments memory accesses and detects data races that actually happen at run time. Cheaper than Miri, less thorough than loom, and useful on real workloads including across FFI boundaries.

Also absent, for the same structural reason: cargo test --test-threads=1, #[should_panic], #[bench], and property-testing crates like proptest — all of them need cargo.

💡Your lock-free queue passes its tests, passes Miri, and passes loom. Is it correct? click to reveal

Closer than almost any code you will write, and still not proven.

Miri checks the executions it runs, under one preemption schedule unless you vary it. It finds UB it observes; it does not prove the absence of UB in executions it did not take.

loom is exhaustive over the interleavings of the test you wrote. If your test uses two threads and your bug needs three, loom will not find it. Its exhaustiveness is over schedules, not over inputs or shapes.

Neither says anything about whether the algorithm is right — whether your queue is actually FIFO, whether it can lose an element under a sequence you never modelled, whether the API’s safety contract is stateable.

Which is why the answer for production code is: use std’s primitives, or crossbeam’s, both of which have had years of adversarial attention. Write your own lock-free structure to learn how they work — as you did in this track — and reach for one you did not write when correctness matters more than the education.

What std still does not give you

Worth knowing so you recognise the gap when you hit it:

  • std::sync::mpmc — a genuine multi-consumer channel. Unstable (mpmc_channel, tracking issue #126840). This is why every std worker pool is written as Arc<Mutex<Receiver<T>>>.
  • std::sync::nonpoison — locks without poisoning. Unstable (sync_nonpoison, #134645), with an open proposal (rust-lang/rust#149359) to make them the Edition 2027 default.
  • std::cell::SyncUnsafeCell — still unstable, which is why people write the two-line wrapper by hand.
  • No reentrant mutex, no lock hierarchy checker, no deadlock detector.

And the crates that fill the gaps, so you know what to reach for:

crate what it gives you
crossbeam mpmc channels, epoch-based reclamation, better sync primitives
rayon data parallelism — par_iter, work stealing, the safe version of everything in this track
parking_lot smaller, faster, non-poisoning locks, optional deadlock detection
dashmap a sharded concurrent map, so you stop writing Arc<Mutex<HashMap>>
loom exhaustive interleaving testing

The disposition to leave with

Rust’s guarantee is genuine and it is narrow: safe Rust eliminates data races. That is an enormous, unusual achievement, and it is not the same as eliminating concurrency bugs.

Deadlocks, livelocks, lost updates, missed wakeups, lost wakeups, wrong memory orderings, ABA, unbounded queues, false sharing, and every ordering assumption you made without writing it down — all of those survive the borrow checker intact, and this course’s grader saw only the ones that happened to bite on one machine on one run.

So: prefer designs where the bug cannot be expressed. Partition instead of sharing. Use a channel when the work has a direction. Take std’s primitives over your own. Write down the ordering you assumed, because nothing else in your toolchain will record it. And treat a green tick as evidence, not proof.

Atomics, Send/Sync and the Memory Model · step 12 of 12

That's the end of this track. Review it or pick another.

← Back to Atomics, Send/Sync and the Memory Model