Skip to content
← All Rust articles

Rust explainer

What passing concurrency tests tells you

About 6 min to read. Allow extra time to work through examples.

Understand the grader's limits and choose additional checks for memory safety, thread schedules and algorithm behavior.

This article uses Rust. You can make it your practice mode to browse related content across the site.

Passing a concurrency exercise is useful evidence. It shows that the submission met the configured compile, lint and test requirements. It does not show that every input, thread schedule or platform will behave correctly.

What the site runs

The Rust runner combines your code with the exercise's harness and builds a single program using rustc -O --edition 2024. It runs the binary against test inputs and checks the configured expectations. A separate Clippy gate uses warnings as errors for the submission's diagnostics.

Code runs on the connected runtime machine. The compiler version, operating system and hardware can affect what a particular execution reveals. The runner does not build a Cargo project or automatically run Miri, Loom or a sanitizer. Those require a separate workflow on your machine.

A test can catch an incorrect result or a timeout. It can also miss a defect that needs a different sequence of operations. A timeout is a symptom to investigate, not a diagnosis of deadlock.

Common gaps in ordinary tests

  • Memory ordering: a program may appear to work with an ordering weaker than its correctness argument requires. Similar machine instructions on one target do not make two Rust atomic orderings interchangeable.
  • ABA: a shared value can change from A to B and back to A. A successful compare-and-exchange only checks the current value; it does not prove that the underlying object or history is unchanged.
  • Unsafe code: an incorrect Send or Sync implementation can invalidate the assumptions used by otherwise safe callers. A plausible output does not rule out undefined behavior.
  • Progress: conflicting lock order, a missed wakeup or a still-live channel sender can prevent completion. Tests can expose these problems, especially with deliberately coordinated schedules, but one passing run does not establish progress under all conditions.

Safe Rust's data-race guarantees rely on sound unsafe implementations below it. They do not eliminate logical races, deadlocks or application-level mistakes such as performing a read and an update as separate operations when they needed to be one transaction.

What Clippy contributes

Compiler and Clippy diagnostics catch many recognizable mistakes. For example, the compiler's let_underscore_lock lint flags a discarded lock guard, and invalid atomic-ordering combinations can be rejected directly. Clippy also suggests clearer expressions and APIs.

The exact diagnostics depend on the toolchain, enabled lints and code shape. A clean lint run is not an audit of lock ordering, cancellation behavior or an unsafe abstraction's contract. Keep the invariants in the code and test the behavior they are meant to protect.

Miri: check executed code for undefined behavior

Miri interprets Rust code and detects many invalid memory operations, including use-after-free, invalid values and data races. It is useful for small tests around unsafe implementations.

Its documented Cargo workflow uses a nightly toolchain and the Miri component. It checks the executions it explores, not every possible caller or schedule. Varying seeds can expose more executions. Platform APIs and foreign code have support limits, and its weak-memory emulation is not complete. A successful run is evidence about that run, not a proof that a public safe API is sound for all valid callers.

Loom: explore a small concurrency model

Loom explores alternative executions of tests written using its replacement synchronization and threading types. Ordinary synchronization hidden in unadapted code is not automatically modeled.

Start with a small operation sequence and explicit assertions: no lost values, correct queue order, or exactly one successful initialization. Exploration can become expensive as the model grows. Bounds and pruning settings reduce the schedules covered, and Loom itself has memory-model limitations. Do not interpret a passing model as coverage of every legal hardware outcome or every application workload.

Loom can check algorithm properties when you write assertions for them. It cannot infer your intended queue semantics from the implementation.

ThreadSanitizer: observe races in instrumented executions

ThreadSanitizer instruments a program to detect data races in executions it runs. Rust's documented integration uses nightly sanitizer options and supported targets. Follow the target-specific setup, including instrumentation of relevant dependencies and the standard library where required.

Incomplete instrumentation can produce misleading reports. Atomic fences and synchronization through inline assembly have documented limitations. A clean run does not establish deadlock freedom or correct use of atomics. Miri, Loom and ThreadSanitizer offer different evidence; they are not a simple ranking from least to most thorough.

Ordinary tests do not require Cargo

The site's submission runner does not invoke Rust's standard test harness, but that is a runner choice. rustc --test supports #[test] and #[should_panic] directly. For example, save this as checks.rs:

#[test]
fn wrapping_boundary() {
    assert_eq!(u8::MAX.wrapping_add(1), 0);
}

#[test]
#[should_panic(expected = "empty input")]
fn empty_input_is_rejected() {
    panic!("empty input");
}

Compile and run it outside the site's runner:

rustc --edition 2024 --test checks.rs -o checks
./checks --test-threads=1

Cargo makes project testing and dependency management more convenient. Restricting test-harness threads to one serializes test functions, not the worker threads created inside each test. See the rustc test guide.

Choose checks for the property you need

Write down what must remain true, including ownership, synchronization and progress assumptions. Then combine ordinary tests, targeted schedule tests and suitable analysis tools. Keep failing cases small enough to understand.

Before implementing a custom concurrent structure, check whether an existing primitive meets the requirements. That can reduce the amount of code whose safety you must establish, while still leaving you responsible for using it correctly. Prefer a clear ownership and synchronization design over relying on repeated successful runs to justify one you cannot explain.

step 12 of 12

Atomics, Send/Sync and the Memory Model

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks