5

A confusing example in cppreference is

#include <thread>
#include <atomic>
#include <cassert>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
 
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst)) {
        ++z;
    }
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst)) {
        ++z;
    }
}
 
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0);  // will never happen
}

The annotation says that only the std::memory_order_seq_cst can guarantee the assert not be fired. However, Isn't std::memory_order_acquire for load and std::memory_order_release for store for the same atomic variable is sufficient here? IMO, all memory writes before M.store(x, std::memory_order_release) in one thread are visible to the reads after M.load(std::memory_order_acquire), for example

int x = 0, y = 0;
std::atomic<int> atomic_i32 = {0};
// Thread 1:
x = 2;
y = 3;
atomic_i32.store(1,std::memory_order_release);
// Thread 2:
while(atomic_i32.load(std::memory_order_acquire)!=1);
assert(x==2); // no fired
assert(y==3); // no fired

This example can demonstrate the effect of the pairing use of std::memory_order_acquire and std::memory_order_release. So, why doesn't this kind of memory order guarantee the first example? The modified version might be

void write_x()
{
    x.store(true, std::memory_order_release);
}
 
void write_y()
{
    y.store(true, std::memory_order_release);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_acquire))  // #1
        ;
    if (y.load(std::memory_order_acquire)) {
        ++z;
    }
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_acquire))  // #2
        ;
    if (x.load(std::memory_order_acquire)) {
        ++z;
    }
}
 
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0);  // fired?
}

Doesn't std::memory_order_acquire also prevent the code from being moved to part before the load? Which makes z at least greater than zero?

xmh0511
  • 7,010
  • 1
  • 9
  • 36
  • 1
    My understanding of this problem is that without sequential consistency, both writes to `x` and `y` may be observed by threads `c` and `d` in a different order. Sequential consistency guarantees a single global ordering observed by all threads. – Daniel Langr Oct 18 '22 at 07:53
  • There is also the wrinkle that the standard *permits* but does not *require* the assert to fire. You need to find an implementation that also permits sequentially inconsistent uses of atomics in this case. – Caleth Oct 18 '22 at 09:16
  • @DanielLangr In that case, how could `z` be `zero`? For example, `x` be true, then the first and second statements of `c` are executed, `z` is zero. However, then the first and statements of `d` are executed, `z` will be `one`. This is the first case, then so forth. Which case `z` would be `zero`? – xmh0511 Oct 18 '22 at 09:45
  • @DanielLangr *When c first observes the write to x and then to y, the if condition might be resolved as false* In this case, the `x` must be true. Then, only `y` is set to true, `d` will enter the if condition. **I cannot figure out the point where `c` and `d` enter their `if condition` that would be false but each other will pass through its while statement**. – xmh0511 Oct 18 '22 at 12:43
  • @xmh0511 (I deleted my previous comment by accident.) `c` reads `x==true` and `y==false`. `d` reads `y==true` and `x==false`. Again, this is possible when there is no global order of writes agreed on by all threads. (You can kind of think that those writes are propagated to both threads so each thread receives two messages. Like `x→c`, `x→d`, `y→c`, and `y→d`. But since they are not synchronized, for `c` and `d` those messages can arrive in a different order, e.g., `x→c` then `y→c`, and `y→d` then `x→d`). – Daniel Langr Oct 18 '22 at 13:43
  • You can find more in this question: [Acquire/Release VS Sequential Consistency in C++11?](https://stackoverflow.com/q/50462948/580083). It seems that your question is actually its duplicate. Another higly-relevant question: [Will two atomic writes to different locations in different threads always be seen in the same order by other threads?](https://stackoverflow.com/q/27807118/580083) – Daniel Langr Oct 18 '22 at 13:54

0 Answers0