Using memory_order_relaxed for storing with memory

2020-07-22 03:33发布

I have question related with following code

#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);
    y.store(true, std::memory_order_relaxed);
}

void read_y_then_x()
{
    while (!y.load(std::memory_order_acquire));
    if (x.load(std::memory_order_acquire))
        ++z;
}

int main()
{
    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);
}

Can i be sure about assert(z.load() != 0) is always be false? I think x.store and y.store is not reordered in data provider thread (that is true?). For this reason i think if thread which loading values stored by x and y uses memory_order_acquire, it was get actual values for x and y from cache of core which performs storing operators.

2条回答
家丑人穷心不美
2楼-- · 2020-07-22 03:56

I think the assert may fail. The std::memory_order_relaxed allows the compiler to reorder the stores inside write_x_then_y. (For example if it thinks it would be faster for any reason.) So it may write y before x. The whole read_y_then_x may happen between those two writes and so it will observe y being true, x being false and it won't increment z.

查看更多
贼婆χ
3楼-- · 2020-07-22 04:09

Although michalsrb already answered it, I'm adding my answer because he started with "I think" ;).

The C++ memory model allows the assert to fail.

Here are some notes:

  • Use #include <cassert>; standard headers don't end on .h.
  • atomic<bool> and atomic<int> are extremely likely to be (lock-free and) POD; by defining them in global namespace they will be initialized with an image of all zeroes; aka, they will have the value false and 0 respectively even before reaching main. However, C++ has a special macro to initialize atomics: ATOMIC_VAR_INIT. Using that guarantees correct initialization and when the variable has static storage duration, this initialization is constant initialization. (When initialized correctly) The three assignments at the beginning of main have no effect. On the other hand, in general (e.g. when the atomic variable is on the stack) there is a difference between an explicit initialization and an assignment: the initial initialization is non-atomic. The assignment is turned into a store with memory order seq_cst. A better style here therefore would be to use ATOMIC_VAR_INIT and leave out the initialization at the start of main:

    std::atomic<bool> x = ATOMIC_VAR_INIT(false);
    std::atomic<bool> y = ATOMIC_VAR_INIT(false);
    std::atomic<int> z = ATOMIC_VAR_INIT(0);
    

Note that if for some reason one needs to (re)initialize an atomic at run time (and not where it is being defined), you should use atomic_init.

  • std::memory_order_acquire only causes a synchronization when reading the value written with a store memory_order_release (which includes memory_order_seq_cst that is both release and acquire). But since you don't have a store with memory_order_release in a different thread, there is certainly not going to be any synchronization going to happen. The initialization in main was seq_cst, but that was done before thread b was even created, so there is already a synchronization there (nl. Also-Synchronizes-With, which is much like the inter-thread Sequenced-Before relationship). Hence, using std::memory_order_relaxed instead of std::memory_order_acquire will do the same thing and using explicitly memory_order_acquire seems a bit strange here.

So then, because there is no synchronization between threads a and b, there is no synchronization between the order in which both threads see changes to x and y and thread b can see y become true before it sees x to become true.

Do not try to understand this with compiler reordering, or hardware pipelines or anything; this is the abstract C++ memory model "computer" which is independent of whatever implementation (compiler) or hardware you might be using. It is simply a fact that this reordering is allowed to take place. With that in mind, thread b can finished and be joined leaving z still at its value of 0.

It might be instructive to see what would happen if you change your program into:

void write_x_then_y()
{
    x.store(true, std::memory_order_relaxed);
    y.store(true, std::memory_order_release); // RELEASE HERE
}

void read_y_then_x()
{
    while (!y.load(std::memory_order_acquire));
    if (x.load(std::memory_order_relaxed))
      ++z;
}

Thread b still will hang on the while until it reads the value true for y. So, it reads the value written by thread 1 with a store memory_order_release! Note that the load of y was still done with memory_order_acquire. Now a synchronization takes place: everything that was written to any memory location before the store/release that we read from will be visible in the thread that did the read/acquire after that read. In other words, now the x.store(true, std::memory_order_relaxed); of thread a will be visible in thread b when it performs the load of x; and the assert will never fail.

查看更多
登录 后发表回答