warming up your workspace

The data race Rust refuses to compile, and the one line that fixes it

A data race is one of the nastiest bugs in software: two threads access the same memory at the same time and at least one is writing, so the result depends on timing and is different every run. In C, C++, Go, or Java, the code that causes it compiles cleanly and passes your tests, then corrupts data in production under load, where you cannot reproduce it. Rust made a promise that sounds too strong to be true: in safe Rust, a data race is a compile error. Not a warning, not a runtime panic, a program that does not build. As Rust cements its place in new systems code through 2026, this is the guarantee people mean when they say "fearless concurrency". Write the race and watch it happen.

The one idea

Rust tracks, at compile time, who owns each piece of data and who is allowed to touch it. The rule is simple and absolute: you can have many readers or one writer, never both at once. A thread is a piece of code that may run for an unknown amount of time, so if you hand data to a thread, Rust has to know that no one else can mutate it meanwhile. When you try to give the same mutable data to several threads, you are asking to violate the one-writer rule, and the compiler simply will not let the ownership work out. The data race is impossible to express in safe code, so it is impossible to ship.

Write the race

Three threads, each trying to modify the same vector:

use std::thread;
fn main() {
    let mut data = vec![1, 2, 3];
    let mut handles = vec![];
    for i in 0..3 {
        handles.push(thread::spawn(move || {
            data[i] += 100;          // each thread mutates the shared vector
        }));
    }
    for h in handles { h.join().unwrap(); }
    println!("{:?}", data);
}

In most languages this compiles and races. Rust stops at the build:

error[E0382]: borrow of moved value: `data`
 6 |         handles.push(thread::spawn(move || {
   |                                    ------- value moved into closure here, in previous iteration of loop
...
11 |     println!("{:?}", data);
   |                      ^^^^ value borrowed here after move

Read what it caught. To give the vector to the first thread, the closure has to take ownership of it, move. But then the second thread cannot also take it, it was already moved, and the third cannot either. There is exactly one data, and ownership can go to exactly one place. The compiler is not being fussy; it is telling you that "three threads share one mutable vector" is a contradiction, and it noticed before you ran it.

Fix it in the one honest way

The fix is to make the sharing explicit and safe: wrap the data in an Arc (an atomically reference-counted handle so several threads can co-own it) around a Mutex (a lock so only one thread touches the inside at a time). Now the type system has proof that access is serialized.

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let counter = Arc::new(Mutex::new(0));      // shared ownership + a lock
    let mut handles = vec![];
    for _ in 0..10 {
        let c = Arc::clone(&counter);           // each thread gets its own handle
        handles.push(thread::spawn(move || {
            let mut n = c.lock().unwrap();       // exclusive access while held
            *n += 1;
        }));
    }
    for h in handles { h.join().unwrap(); }
    println!("final counter: {}", *counter.lock().unwrap());
}
final counter: 10

Three details that matter:

  • Arc::clone does not copy the data, it makes another handle to the same data and bumps a thread-safe reference count. Every thread co-owns the counter, and the memory is freed only when the last handle is gone. That solves "who owns it": everyone, safely.
  • Mutex solves "one writer at a time". You cannot read the number without calling lock(), and while you hold the lock no other thread can. The one-writer rule is now enforced at runtime by the lock, and the type system knows it, so it lets the code compile.
  • Rust could tell the first version was unsafe and the second was safe using two marker traits, Send and Sync, that every type carries. A plain Mutex-wrapped value is Send; a bare &mut shared across threads is not. The whole guarantee reduces to the compiler checking these markers, no runtime race detector required.

Where this shows up

This is why Rust is being pulled into browsers, operating system kernels, and the infrastructure that government memory-safety guidance now pushes hard. Concurrency bugs are among the most expensive and least reproducible in all of software, and Rust converts a whole class of them from "found in production, maybe" to "found at compile time, always". The same ownership rules that stop the data race also stop use-after-free and iterator invalidation, one model catching a family of bugs.

If you want to build up from ownership to channels, atomics, and lock-free structures, all checked by the same compiler, that is the path the rust track on IWTLP takes one guarantee at a time.

Sources