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
andd
to observe changes to the atomicsx
andy
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.