Step 4 of 6
Global state without static mut
A static is a global variable with one fixed address for the whole program. Immutable statics are perfectly safe. A static mut can be changed, and that's the problem: any thread can reach it at any time, so two threads incrementing it at once is a data race, which is UB. The compiler can't prevent that, so every access to a static mut is unsafe.
Edition 2024 goes further and forbids even taking a reference to one:
static mut COUNTER: u32 = 0;
fn main() {
unsafe {
COUNTER += 1;
println!("{}", COUNTER);
}
}
error: creating a shared reference to mutable static
(println! takes its arguments by reference.) Reading the value out (let n = unsafe { COUNTER };) still compiles, but the race is still there. In modern Rust, static mut is almost never the answer.
The safe tools
- Atomics (from the sync module) for counters and flags:
AtomicU64,AtomicBool, ... They can live in a plainstaticbecause their methods take&self. Mutex<T>in astaticfor anything bigger.Mutex::newis aconst fn, sostatic LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());just works.OnceLockandLazyLockfor globals that are built once at runtime (a parsed config, a compiled regex).
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
static REQUESTS: AtomicU64 = AtomicU64::new(0);
static ERRORS: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn handle(id: u64) {
REQUESTS.fetch_add(1, Ordering::Relaxed);
if id % 500 == 0 {
ERRORS.lock().unwrap().push(format!("request {id} failed"));
}
}
fn main() {
let workers: Vec<_> = (0..4)
.map(|w| {
thread::spawn(move || {
for i in 0..1000 {
handle(w * 1000 + i);
}
})
})
.collect();
for worker in workers {
worker.join().unwrap();
}
println!("requests: {}", REQUESTS.load(Ordering::Relaxed));
println!("errors: {}", ERRORS.lock().unwrap().len());
}
requests: 4000
errors: 8
Four threads, no unsafe, and the counts are always right. fetch_add returns the previous value and adds atomically; Ordering::Relaxed is enough for a counter that nothing else depends on. Global state still makes code harder to test (every test shares it), so at work prefer passing state in (Arc<AppState>) and keep statics for things like metrics.
Unsafe traits
A trait can be declared unsafe trait when implementing it makes a promise the compiler can't check. The ones you'll meet are Send and Sync. The compiler implements them automatically when a type's fields are thread-safe; a struct holding a raw pointer gets neither. If you've made sure such a type really is safe to send between threads, you say so with an unsafe impl:
struct DeviceHandle {
regs: *mut u32, // raw pointer: not Send by default
}
// SAFETY: the device registers may be used from any thread, and every
// access goes through methods that take &mut self.
unsafe impl Send for DeviceHandle {}
Unions
A union looks like a struct, but all fields share the same memory, like a C union. The compiler doesn't know which field was written last, so reading one is unsafe:
union Bits {
f: f32,
u: u32,
}
fn main() {
let b = Bits { f: 1.0 };
// SAFETY: every bit pattern is a valid u32, so reading `u` is fine.
let raw = unsafe { b.u };
println!("{raw:#010x}");
println!("{:#010x}", 1.0f32.to_bits());
}
0x3f800000
0x3f800000
Unions exist mainly to match C structs in FFI. For everything else there are safe tools: f32::to_bits, u32::from_le_bytes and an enum (a union with a tag that Rust checks for you).
Your task
next_ticket hands out ticket numbers 1, 2, 3, ... and is called from many threads, but it's built on a static mut, which races. Rewrite it with a static AtomicU32 and fetch_add, with no unsafe and no static mut.
Challenge 2
This logger was written for an older edition with a static mut Vec, and edition 2024 refuses to compile it (taking &mut of a static mut is an error). Rewrite it with a static LOG: Mutex<Vec<String>>: log appends a message, and take_logs returns all messages so far (oldest first) and leaves the log empty. No unsafe, no static mut.
Challenge 3
Fill in the blanks to declare a union and read one of its fields. Both fields share the same 4 bytes, so writing word and reading bytes shows how the number is laid out in memory. On little-endian CPUs (x86 and most ARM), the lowest byte comes first, which is exactly what the safe to_le_bytes gives. The program should print [4, 3, 2, 1] twice.
Previous: Calling C with FFI Next: Undefined behavior and Miri