What happens if we replace SeqCst with Acquire-Release for this code snippet?

This code snippet is from the cppreference site. I only replaced sequentially consistent ordering with acquire-release release to understand how the assertion fails(z remains 0) as the article mentioned:

This example demonstrates a situation where sequential ordering is necessary. Any other ordering may trigger the assert because it would be possible for the threads c and d to observe changes to the atomics x and y in the opposite order.

Here is the Rust's equivalent code:


use std::sync::atomic::{AtomicBool, AtomicI32, Ordering::{Acquire, Release}};
use std::thread;

static X: AtomicBool = AtomicBool::new(false);
static Y: AtomicBool = AtomicBool::new(false);
static Z: AtomicI32 = AtomicI32::new(0);

fn main()
{
    let thread_a = thread::spawn(||write_x());
    let thread_b = thread::spawn(||write_y());
    let thread_c = thread::spawn(||read_x_then_y());
    let thread_d = thread::spawn(||read_y_then_x());
    thread_a.join().unwrap();
    thread_b.join().unwrap();
    thread_c.join().unwrap();
    thread_d.join().unwrap();

    assert!(Z.load(Acquire) != 0); // may happen

}

fn write_x()
{
    X.store(true, Release);
}

fn write_y()
{
    Y.store(true, Release);
}

fn read_x_then_y()
{
    while !X.load(Acquire) { }

    if Y.load(Acquire)
    {
        Z.fetch_add(1, Acquire);
    }
}

fn read_y_then_x()
{
    while !Y.load(Acquire) { }

    if X.load(Acquire)
    {
        Z.fetch_add(1, Acquire);
    }
}

The assertion might fail when using the Acquire-Release combination for the above code snippet; Here is my reasoning:

The thread_c waits for the X to become true — from the while loop while !X.load(Acquire) { } . Then the thread_c will check the Y’s value to increment the Z. The write/store of true to the X happens in thread_a using the Release ordering, and thread_c waits for the X becomes to true then we can say that there is a happens-before relationship between thread_a and thread_c or the thread_a synchronizes with the thread_c for the atomic variable X. So it is a guarantee that thread_c will observe the write to X by thread_a. Now the increment depends on the Y's value to be observed as true in thread_c. Write/store to the Y happens in thread_b. There is no happens-before or synchronizes-with relationship between the thread_b and thread_c. The thread_c does not wait for the Y to be observed as true. So increment to Z might not happen. The Z remains zero for this case.

In thread_d we wait for the Y to be observed as true which is stored/written by thread_b . Hence there is a happens-before relationship between the thread_d and thread_b, or they synchronize with each other for the Y’s value to be observed as true . However the increment of Z depends on the X to be observed as true which is stored/written by thread_a. As the thread_d do not wait for the X’s value to be observed as true so there is no happens-before or synchronizes-with relationship between the thread_d and thread_a. Hence the Z might remain zero. As a result, the assert in the main function might fail.

These are my reasons why Z might remain zero or the assertion might fail. Please correct me if I am wrong.

Confusing part: Say thread_d is done with execution which means Y is true — it is a guarantee as there is a happens-before relationship with thread_b and thread_d. No increment happened to Z because Y is observed as false in thread_d. Now say the thread_c is executing its code, this thread will observe the X’s value as true. Now it is going to check the Y’s value to increment the Z. The Y is already true as thread_d is done execution and this thread ensures that Y is true. Then increment to Z must happen here, right? Hence the assert won’t fail even using the Acquire-Release combination. What am I missing here to connect the dots?

However, the preference says using the Acquire-Release combination might lead to assertion failure.

Attempt to clear my confusion: thread_c may find Y as false as there is no happens-before or synchronization with the relationship between the thread_c (loading Y) and thread_b(storing true to Y). Please correct me if my attempt here is incorrect.

Each atomic variable has a single modification order. (Independent from others.)

Only to thread_d (and thread_b)
The other thread_c still can get the earlier value.

Only SeqCst adds;
"the additional guarantee that all threads see all sequentially consistent operations in the same order."

1 Like