#276 Jul 23, 2026

276. AtomicBool::swap — Let Exactly One Thread Claim the Job

“Check the flag, then set it” has a gap where two threads both see false — and your run-once code runs twice. swap sets and reads the flag in one atomic step.

Say only one thread should print a deprecation warning, spawn the background worker, or run cleanup. The obvious flag check is broken:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::sync::atomic::{AtomicBool, Ordering};

static WARNED: AtomicBool = AtomicBool::new(false);

fn warn_once() {
    // RACE: both threads can see false
    if !WARNED.load(Ordering::Relaxed) {
        WARNED.store(true, Ordering::Relaxed);
        eprintln!("legacy config detected");
    }
}

Same check-then-act race as yesterday’s fetch_max story: two threads load false, both pass the if, both warn. The fix is again a single atomic call:

1
2
3
4
5
6
fn warn_once() {
    // one atomic op: first caller wins
    if !WARNED.swap(true, Ordering::Relaxed) {
        eprintln!("legacy config detected");
    }
}

swap stores the new value and returns the previous one as a single atomic operation. Only the first caller gets false back — everyone else sees true and skips the block. No mutex, no window to slip through:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};

static CLAIMED: AtomicBool = AtomicBool::new(false);
static RUNS: AtomicU32 = AtomicU32::new(0);

std::thread::scope(|s| {
    for _ in 0..8 {
        s.spawn(|| {
            if !CLAIMED.swap(true, Ordering::Relaxed) {
                RUNS.fetch_add(1, Ordering::Relaxed);
            }
        });
    }
});
assert_eq!(RUNS.load(Ordering::Relaxed), 1);

Relaxed is fine while the flag itself is the only shared state. The moment the winning thread writes data that losers will read, you need Acquire/Release ordering — or just reach for OnceLock, which handles that (and blocking until init finishes) for you.

Where swap beats Once/OnceLock: it produces no value, it never blocks the losers, and you can re-arm it — store(false, ...) resets the flag for the next round. Think “claim ticket”, not “lazy init”.

Works on every Atomic* type, stable since Rust 1.0.

← Previous 275. fetch_max — Track a High-Water Mark Without the Race Next → 277. Release/Acquire — Publish Data Through a Flag, Not Just the Flag