Concurrency Fundamentals

Data Races

A data race occurs when two or more threads access the same memory location concurrently, at least one of the accesses is a write, and at least one of the accesses is unsynchronized (neither protected by a mutual-exclusion lock nor performed using atomic instructions). In Rust (as in C and C++), a data race is immediate undefined behavior—and Safe Rust guarantees at compile time that data races are impossible.

The Three Ingredients Required for a Data Race

All three conditions must hold simultaneously for a data race to occur. Safe Rust breaks at least one of these three conditions on every memory access:

The Data Race Triangle (Break ANY Edge to Prevent Data Races):

          (1) Same Memory Location Accessed by >= 2 Threads
                         /                    \
                        /                      \
                       /                        \
  (2) At Least One Access Is a Write -------- (3) Unsynchronized (Non-Atomic / Unlocked)
Condition Required for a Data Race How Safe Rust Prevents It
1. Shared across threads Types without Send/Sync (Rc<T>, Cell<T>, RefCell<T>) cannot cross thread boundaries (E0277)
2. Concurrent write + read/write Borrow checker enforces many &T (read-only) XOR one &mut T (exclusive write); &mut T cannot alias
3. Unsynchronized mutation Shared mutation across threads is only possible through Sync types (Atomic*, Mutex<T>, RwLock<T>) that synchronize all accesses

Compiler Rejection of an Unsynchronized Cross-Thread Write (E0277 / E0382)

If two threads attempt to increment the same variable without synchronization, Safe Rust rejects the program at compile time:

  • Attempting to capture &mut count in two threads fails with E0499 (cannot borrow count as mutable more than once at a time).
  • Attempting to share Cell<u64> across threads fails with E0277 (Cell<u64> cannot be shared between threads safely):
// ❌ does not compile
use std::cell::Cell;
use std::thread;

fn main() {
    let counter = Cell::new(0_u64);

    thread::scope(|s| {
        // Error E0277: `&Cell<u64>` is NOT `Send` because `Cell<u64>` is `!Sync`!
        s.spawn(|| counter.set(counter.get() + 1));
        s.spawn(|| counter.set(counter.get() + 1));
    });
}
error[E0277]: `Cell<u64>` cannot be shared between threads safely
  --> src/main.rs:9:17
   |
 9 |         s.spawn(|| counter.set(counter.get() + 1));
   |           ----- ^^ `Cell<u64>` cannot be shared between threads safely
   |           |
   |           required by a bound introduced by this call
   |
   = help: the trait `Sync` is not implemented for `Cell<u64>`
   = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicU64` instead
   = note: required for `&Cell<u64>` to implement `Send`

Replacing Cell<u64> with AtomicU64 (as suggested directly by the compiler help note!) makes the concurrent increments lock-free and completely free of data races:

use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;

fn main() {
    let counter = AtomicU64::new(0);

    thread::scope(|s| {
        for _ in 0..4 {
            s.spawn(|| {
                for _ in 0..1_000 {
                    counter.fetch_add(1, Ordering::Relaxed);
                }
            });
        }
    });

    println!("race-free atomic total: {}", counter.load(Ordering::Relaxed));
}
race-free atomic total: 4000

Data Races vs. General Race Conditions

It is important to distinguish a data race (low-level undefined behavior on unsynchronized memory bytes) from a logical race condition (high-level non-deterministic ordering of synchronized operations):

Property Data Race Logical Race Condition
Level Memory model / hardware byte access Application business logic / lock ordering
Causes Undefined Behavior (UB)? Yes (tearing, register caching, optimizer corruption) No (memory remains valid and well-defined)
Possible in Safe Rust? Impossible (prevented at compile time) Possible (e.g., releasing a lock between a check and an update — TOCTOU)
How to prevent Enforced automatically by Send, Sync, and borrow checker Hold a single MutexGuard across the entire check-and-update transaction, or use atomic compare_exchange