Is the return value of @cmpxchgWeak() accurate when a sporadic failure occurs?

In the following code, the exchange operation would succeed when the value is 0. The return value would be null. If it’s not 0, then its current value is returned:

if (@cmpxchgWeak(&value, 0, 123, .monotonic)) |current_value| {
    switch (current_value) {
        0 => {
            // sporadic failure
        },
        else => {
            // expected failure
        }, 
    }
}

So what happens when a sporadic failure (which would not happen if the strong version of the builtin is used instead) occurs? Would current_value be 0? Or perhaps it can never be 0?

According to the documentation it is equivalent to the following code:

fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_value: T) ?T {
    const old_value = ptr.*;
    if (old_value == expected_value and usuallyTrueButSometimesFalse()) {
        ptr.* = new_value;
        return null;
    } else {
        return old_value;
    }
}

Here we can see that on sporadic failures it goes into the else branch and returns the old value, so in your example current_value would be 0.

If the pseudo-code is correct, then the following would sporadically deadlock:

while (true) {
    if (mutex_value.cmpxchgWeak(0, 123, .acquire, .monotonic)) |current_value| {
        // sleep until mutex_value is not current_value
        std.Thread.wait(&mutex_value, current_value);
    } else break;
}

Because std.Thread.wake() would only be called when the mutex is released (mutex_value = 0), std.Thread.wait() would never see a change if current_value is 0.

Yeah, it would deadlock.
Where did you get that code anyways? Neither of Thread.wait nor Thread.wake exist in std.

Oh, I left out “Futex”. std.Thread.Futex.wait and std.Thread.Futex.wake.

Anyway, I decided to check the Godbolt output for aarch64 (cmpxchg is always strong in x64) just to be sure. Here’s the input code:

const std = @import("std");

var ai = std.atomic.Value(i32).init(8);

export fn change() i32 {
    if (ai.cmpxchgWeak(0, 1, .acquire, .monotonic)) |value| {
        return value;
    } else {
        return 2;
    }
}

And the output:

change:
        adrp    x9, example.ai
        add     x9, x9, :lo12:example.ai
        ldaxr   w8, [x9]
        cbz     w8, .LBB0_2
        mov     w9, wzr
        clrex
        b       .LBB0_3
.LBB0_2:
        mov     w10, #1
        stxr    w11, w10, [x9]
        cmp     w11, #0
        csetm   w9, eq
.LBB0_3:
        tst     w9, #0x1
        mov     w9, #2
        csel    w0, w9, w8, ne
        ret

example.ai:
        .word   8

It does appear that the pseudo code is correct. STXR is the instruction that can fail. The earlier result from LDAXR would then be selected by CSEL, which is going to be 0.

So it’s necessary to make the call to wait() conditional on current_value != 0.