Help me understand Arc bug


#1

This issue changes Arc but I don’t understand why. To me the bug is with Mutex get_mut() and Arc is correct. The second sample code is said to “exhibit the same problem” with no mutex.

To me this second code is practically the same as;

fn main() {
    use std::sync::atomic::{AtomicBool,Ordering};
    static mut MEM:u32 = 0;
    static CAN_WRITE:AtomicBool = AtomicBool::new(false);

    let g = std::thread::spawn(|| {
        let val = unsafe { MEM };
        CAN_WRITE.store(true, Ordering::Release);
        assert_eq!(val,0);
    });
    
    while !CAN_WRITE.load(Ordering::Relaxed) {}
    unsafe { MEM = 1; }
    
    g.join().unwrap();
}

I don’t see the problem with just Relaxed. (My view is the compiler/CPU can only move the assert to before the store.) Or if not a problem here how does it differ from the none-mutex code.

(First found about it here with blog.)


#2

Did not give it extended thought, but I think the problem is in this part:

while !CAN_WRITE.load(Ordering::Relaxed) {}
unsafe { MEM = 1; }

The compiler and the CPU are probably allowed to reorder the MEM write before the while loop on CAN_WRITE.load(), because these touch different regions of memory and there is no memory barrier between them.

Sure, from a control flow point of view, reordering the two lines of code involves assuming that the loop will terminate. But LLVM’s optimizer is well-known for making such assumptions.


#3

That’s exactly it - Relaxed has zero ordering with other operations; all it essentially guarantees is:

  1. The load is atomic (eg 64 bit value on 32 bit system will not tear)
  2. The load will issue (ie compiler won’t remove it)

https://en.cppreference.com/w/cpp/atomic/memory_order has good description of them all.


#4

Will read it in a while, (when more time) so may include this next question.

Would this work in stopping reordering/race? (To me I assumed the test in while would be enough but can see there is some independence in that case.)

while !CAN_WRITE.load(Ordering::Relaxed) {}
if CAN_WRITE.load(Ordering::Relaxed)  {
    unsafe { MEM = 1; }
}

#5

This relies on control flow dependence, which isn’t guaranteed - compilers and CPUs can speculate across it.


#6

I thought the “no out of thin air store” guarantee could save us here, am I mistaken?


#7

Where is that stipulated for non-atomic memory?


#8

So this guarantee is only provided for atomic memory? I guess that is the part which I was missing.

EDIT: Oh, and anyhow, looking at the OP’s code…

while !CAN_WRITE.load(Ordering::Relaxed) {}
if CAN_WRITE.load(Ordering::Relaxed) {
    unsafe { MEM = 1; }
}

…with relaxed atomics, the compiler is allowed to optimize out the second if statement by coalescing the CAN_WRITE load inside the loop with that in the if statement’s conditional, resulting in non-conditional execution. So there’s that.


#9

AFAIK, c++ (which Rust follows here) does not make any promises about not introducing phantom reads or writes of plain memory. Atomic variables carry stricter requirements.

This I’m less sure about, although I follow what you’re saying. Technically, that’s an elimination of an atomic load that’s sequenced (ie program order) with the previous loop. This gets into the “optimizations of atomics” topic, which is very subtle and I’d be uncomfortable talking about with any certainty :slight_smile:.