Why can AtomicUsize's fetch_max return a value greater than its argument?

From the description of AtomicUsize's fetch_max, it seems like the return value should always be smaller than or equal to the argument passed to the val parameter.

However, consider the following code:

use rand::Rng;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;

const fn generate_bools() -> [bool; 100] {
    // An array of false values, except for certain indexes.
    let mut bools = [false; 100];
    bools[2] = true;
    bools[7] = true;
    bools[12] = true;
    bools[14] = true;
    bools[22] = true;
    bools[30] = true;
    bools[36] = true;
    bools[41] = true;
    bools[43] = true;
    bools[53] = true;
    bools[56] = true;
    bools[63] = true;
    bools[72] = true;
    bools[79] = true;
    bools[82] = true;
    bools
}

const BOOLS: [bool; 100] = generate_bools();

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    // Spawn 10 threads, for counter increments of +1 to +10 inclusively.
    for inc in 1..=10 {
        let counter = counter.clone();
        std::thread::spawn(move || loop {
            // Load the current counter and add the increment for this thread.
            let current = counter.load(Ordering::SeqCst);
            let new = current + inc;

            // Wait for a random duration between 100 and 355 ms.
            let random = rand::thread_rng().gen::<u8>() + 100;
            std::thread::sleep(Duration::from_millis(random as u64));

            // If the array is true at that index, "fetch_max" it!
            let is_true = BOOLS[new];
            if is_true {
                let old = counter.fetch_max(new, Ordering::SeqCst);
                println!("old: {:02} / new: {:02}", old, new);
            }
            std::thread::sleep(Duration::from_secs(1));
        });
    }
    // Put main thread to sleep for 60s to give time for the children threads.
    std::thread::sleep(Duration::from_secs(60));
}

When I run it, I get unexpected results. Here are two samples:

old: 00 / new: 02
old: 02 / new: 07
old: 07 / new: 14
old: 14 / new: 22
old: 22 / new: 30
old: 30 / new: 36
old: 36 / new: 43
old: 43 / new: 53
old: 53 / new: 63
old: 63 / new: 72
old: 72 / new: 56 <--
old: 72 / new: 79
old: 79 / new: 82
old: 00 / new: 02
old: 02 / new: 07
old: 07 / new: 12
old: 12 / new: 14
old: 14 / new: 22
old: 22 / new: 30
old: 30 / new: 36
old: 36 / new: 43
old: 43 / new: 41 <--
old: 43 / new: 53
old: 53 / new: 56
old: 56 / new: 63
old: 63 / new: 63
old: 63 / new: 72
old: 72 / new: 79
old: 79 / new: 82

I understand that the "new" values might not always go up, because there might be a race condition with the fetch_max and println! statements. For example, thread A can do the fetch_add, then thread B can do the fetch_add and the println!, then thread A can do the println!.

What I don't understand is how the old value can be strictly greater than the new value! Thank you for any help!

The returned value is always the previous value of the atomic, whether that was the larger or smaller of the two values. For example:

    let x = AtomicUsize::new(1);
    let new = 0;
    let old = x.fetch_max(new, Ordering::SeqCst);
    assert_eq!((old, new), (1, 0));
    // `x` still contains `1`

(Playground)

The docs recommend this if you want to get the max value rather than the old value:

let max = x.fetch_max(new, Ordering::SeqCst).max(new);
3 Likes

My bad for the brain fart. Thank you, I appreciate the quick answer!