calvin
calvin

Reputation: 2935

memory_order_relaxed not work as expected in code from C++ concurrency in action

Before asking this question I tried set my VM cores to more than 1(I set it to 2), according to suggestion from this question, this solution worked for the author, but not for me.

The book C++ Concurrency in Action gives an example in Listing 5.5

#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;
std::atomic<int> z;

void write_x_then_y()
{
  x.store(true,std::memory_order_relaxed);  // 1
  y.store(true,std::memory_order_relaxed);  // 2
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));  // 3
  if(x.load(std::memory_order_relaxed))  // 4
    ++z;
}
int main()
{
    for(int i = 0; i < 1000; i++){
      // this loop is addeed by me in order to try 1000 times to see whether the assertion will fail
      x=false;
      y=false;
      z=0;
      std::thread a(write_x_then_y);
      std::thread b(read_y_then_x);
      a.join();
      b.join();
      assert(z.load()!=0);  // 5
    }
}

The book says the assertion assert(z.load()!=0) may fail. However, I can't fail this assertion when testing.

I tested the program by 64-bit Ubuntu(under VMWare), g++ -g -o test --std=c++17 test.cpp -lpthread, I tested under both gcc 5.4.0 and gcc 7.2.0.

I also tested the program by 64-bit Windows 10(not in a VM), Visual studio 2015.

Meanwhile, by searching the internet, I saw some people say this is because of the x86 architecture, however, I remember in x86, Loads may be reordered with older stores to different locations, so I think there can be a order 2-3-4-1 which can fail this assertion.

Upvotes: 2

Views: 1896

Answers (1)

Passer By
Passer By

Reputation: 21131

The book is correct, the assertion may fail, just not on x86 as you suspected.

To be clear, atomics make the specific guarantee that no data races will occur. It doesn't by itself guarantee absence of race conditions.

std::memory_order_relaxed is the loosest of all memory orders. It is the baseline of everything. In practice, it prevents compiler optimizations around the variables.

atomic<bool> x, y;
x.store(true,std::memory_order_relaxed);
y.store(true,std::memory_order_relaxed);

// is similar to...
volatile bool x, y;
x = true;
asm volatile("" ::: "memory");  // Tells compiler to stop optimizing here,
                                // but issues no instructions
y = true;

However, the CPU might itself decide to reorder the writes or the cache system might decide to publish the write to x some time later. This ultimately leads to the assertion fail.

In terms of the language, this is due to the writes have no happens before relation with the reads and therefore have no guarantees on their results.

Finally, the reason you see no failed assertions on a x86 machine is because it inherently has acquire and release semantics on its reads and writes.

// On x86, effectively
void write_x_then_y()
{
    x.store(true,std::memory_order_release);  // 1
    y.store(true,std::memory_order_release);  // 2
}
void read_y_then_x()
{
    while(!y.load(std::memory_order_acquire));  // 3
    if(x.load(std::memory_order_acquire))  // 4
        ++z;
}

Which is to say, if y is determined to be true at // 3, what happened before //2 is guaranteed to be visible after // 3.

Upvotes: 4

Related Questions